Files
kubevela/pkg/definition/defkit/patch_container.go
Ayush Kumar 3f7ad2ba95 Feat: defkit comp def discrepancies (#7048)
* Feat: add MapVariant operation and support for OneOf parameters with default variant

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance string parameter output to include optional prefix in CUE generation

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: add ConditionalOrFieldRef for fallback handling and support inline array values

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: update CUE generation to support inline arrays with conditional wrapping

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: update CUE generation to support inline arrays with conditional wrapping

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: add support for compound optional fields and enhance array builder with guarded filtering

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: add comprehensive tests for ArrayBuilder functionality

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: implement field grouping in StatusBuilder for consolidated CUE output

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance CUE decomposition to support condValues and improve filtering logic

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: add metadata labels to ComponentDefinition and update CUE generation

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: clean up comments and formatting in cuegen and param files

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance CUE generation, trait definitions, and PatchContainer logic

CUE Generation:
- Simplify condition decomposition logic and rename GetDirective method
- Add condition decomposition and lifting logic to improve generated CUE output
- Refactor cueTypeForParamType to use a standalone cueTypeStr function for reusability

Collections:
- Enhance MapVariant operation to merge variant mappings and preserve non-matching items

Trait Definitions:
- Enhance TraitDefinition and PatchContainerConfig with new attributes (MultiContainerCheckField, MultiContainerErrMsg)
- Update emission logic in TraitCUEGenerator for multi-container support

PatchContainer:
- Update error messages to use camelCase for consistency with KubeVela conventions

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance test descriptions for clarity and accuracy in array_builder, collections, and status tests

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance handling of optional fields in collections and improve test descriptions for clarity

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: improve ConditionalOrFieldRef tests for clarity and accuracy in handling primary and fallback fields

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance test descriptions for clarity and accuracy in array_builder and expr tests

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: improve formatting of test data in collections tests for better readability

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

---------

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
2026-03-01 21:29:18 -08:00

546 lines
19 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
// This file extends the fluent API with patterns commonly used in traits:
// - PatchContainerConfig: for traits that patch containers by name
// - LetBinding: for CUE let bindings (local variables)
// - ListComprehension: for CUE list comprehensions with conditional fields
// - ParamIsSet/ParamNotSet: convenience constructors for parameter existence conditions
// PatchFieldBuilder provides a fluent API for constructing PatchContainerField values.
// Use PatchField() to start building.
//
// Example:
//
// defkit.PatchField("exec").IsSet().Build()
// defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0").Build()
// defkit.PatchField("image").Strategy("retainKeys").Description("Specify the image").Build()
type PatchFieldBuilder struct {
paramName string
targetField string
patchStrategy string
condition string
paramType string
paramDefault string
description string
}
// PatchField starts building a PatchContainerField with the given parameter name.
// The TargetField defaults to the same as the parameter name.
func PatchField(name string) *PatchFieldBuilder {
return &PatchFieldBuilder{
paramName: name,
targetField: name,
}
}
// Target sets the container field to patch, if different from the parameter name.
func (b *PatchFieldBuilder) Target(t string) *PatchFieldBuilder {
b.targetField = t
return b
}
// Default sets an explicit default value for the parameter.
func (b *PatchFieldBuilder) Default(val string) *PatchFieldBuilder {
b.paramDefault = val
return b
}
// Type sets an explicit CUE type string (e.g., "string", "[...string]", "{...}").
func (b *PatchFieldBuilder) Type(t string) *PatchFieldBuilder {
b.paramType = t
return b
}
// Int is shorthand for Type("int").
func (b *PatchFieldBuilder) Int() *PatchFieldBuilder {
return b.Type("int")
}
// Bool is shorthand for Type("bool").
func (b *PatchFieldBuilder) Bool() *PatchFieldBuilder {
return b.Type("bool")
}
// Str is shorthand for Type("string").
func (b *PatchFieldBuilder) Str() *PatchFieldBuilder {
return b.Type("string")
}
// StringArray is shorthand for Type("[...string]").
func (b *PatchFieldBuilder) StringArray() *PatchFieldBuilder {
return b.Type("[...string]")
}
// Strategy sets the patch strategy (e.g., "replace", "retainKeys").
func (b *PatchFieldBuilder) Strategy(s string) *PatchFieldBuilder {
b.patchStrategy = s
return b
}
// --- Condition methods (following param.go / health_expr.go patterns) ---
// IsSet guards the field with an existence check (CUE: != _|_).
// Use this for optional fields that should only be patched when provided.
func (b *PatchFieldBuilder) IsSet() *PatchFieldBuilder {
b.condition = "!= _|_"
return b
}
// NotEmpty guards the field with a non-empty string check (CUE: != "").
// Use this for string fields that should only be patched when non-empty.
func (b *PatchFieldBuilder) NotEmpty() *PatchFieldBuilder {
b.condition = `!= ""`
return b
}
// Eq sets a condition that checks the field equals the given value.
func (b *PatchFieldBuilder) Eq(val string) *PatchFieldBuilder {
b.condition = "== " + val
return b
}
// Ne sets a condition that checks the field is not equal to the given value.
func (b *PatchFieldBuilder) Ne(val string) *PatchFieldBuilder {
b.condition = "!= " + val
return b
}
// Gt sets a condition that checks the field is greater than the given value.
func (b *PatchFieldBuilder) Gt(val string) *PatchFieldBuilder {
b.condition = "> " + val
return b
}
// Gte sets a condition that checks the field is greater than or equal to the given value.
func (b *PatchFieldBuilder) Gte(val string) *PatchFieldBuilder {
b.condition = ">= " + val
return b
}
// Lt sets a condition that checks the field is less than the given value.
func (b *PatchFieldBuilder) Lt(val string) *PatchFieldBuilder {
b.condition = "< " + val
return b
}
// Lte sets a condition that checks the field is less than or equal to the given value.
func (b *PatchFieldBuilder) Lte(val string) *PatchFieldBuilder {
b.condition = "<= " + val
return b
}
// RawCondition sets a raw CUE condition string.
// Use this as an escape hatch for non-standard conditions not covered by the typed API.
func (b *PatchFieldBuilder) RawCondition(c string) *PatchFieldBuilder {
b.condition = c
return b
}
// Description sets the +usage description for this field.
func (b *PatchFieldBuilder) Description(d string) *PatchFieldBuilder {
b.description = d
return b
}
// Build returns the constructed PatchContainerField.
func (b *PatchFieldBuilder) Build() PatchContainerField {
return PatchContainerField{
ParamName: b.paramName,
TargetField: b.targetField,
PatchStrategy: b.patchStrategy,
Condition: b.condition,
ParamType: b.paramType,
ParamDefault: b.paramDefault,
Description: b.description,
}
}
// PatchFields builds a slice of PatchContainerField from builders.
// This eliminates the need to call .Build() on each field individually.
//
// Example:
//
// Fields: defkit.PatchFields(
// defkit.PatchField("exec").IsSet(),
// defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0"),
// )
func PatchFields(builders ...*PatchFieldBuilder) []PatchContainerField {
fields := make([]PatchContainerField, len(builders))
for i, b := range builders {
fields[i] = b.Build()
}
return fields
}
// PatchContainerField defines a field to be patched in the container.
type PatchContainerField struct {
ParamName string // the parameter name (e.g., "command", "args")
TargetField string // the container field to patch (e.g., "command", "args")
PatchStrategy string // the patch strategy (e.g., "replace", "merge")
Condition string // optional CUE condition (e.g., "!= null")
ParamType string // explicit CUE type (e.g., "string", "[...string]", "{...}")
ParamDefault string // explicit default value (e.g., "0", "\"\"", "false")
Description string // optional +usage description (auto-generated if empty)
}
// PatchContainerGroup defines a group of fields under a common parent field.
// This generates CUE like:
//
// startupProbe: {
// if _params.exec != _|_ { exec: _params.exec }
// if _params.httpGet != _|_ { httpGet: _params.httpGet }
// }
type PatchContainerGroup struct {
TargetField string // the parent field name (e.g., "startupProbe", "securityContext")
Fields []PatchContainerField // fields within this group
SubGroups []PatchContainerGroup // nested groups (e.g., securityContext.capabilities)
}
// PatchContainerConfig configures the PatchContainer helper.
type PatchContainerConfig struct {
ContainerNameParam string // parameter for container name
DefaultToContextName bool // default container name to context.name
PatchFields []PatchContainerField // flat fields to patch directly on container
Groups []PatchContainerGroup // grouped fields (e.g., startupProbe: { ... })
AllowMultiple bool // if true, allow patching multiple containers
ContainersParam string // for multi-container mode, the array param name
ContainersDescription string // +usage description for the containers param (auto-generated if empty)
CustomParamsBlock string // custom CUE block for #PatchParams (for complex types)
MultiContainerParam string // alternate name for multi-container param (default: "probes" for probes, "containers" for others)
MultiContainerCheckField string // field name for multi-container error check (default: "containerName")
MultiContainerErrMsg string // custom error message for multi-container mode (default: "container name must be set for %s")
CustomPatchContainerBlock string // custom CUE block for PatchContainer body (for complex merge logic)
CustomPatchBlock string // custom CUE block for the patch: spec: template: spec: { ... } body
CustomParameterBlock string // custom CUE block for the parameter definition
PatchStrategy string // if set, emitted as // +patchStrategy=<value> before the patch block (e.g., "open")
ParamsTypeName string // custom name for the #PatchParams helper (default: "PatchParams")
NoDefaultDisjunction bool // if true, omit the * default marker on parameter disjunction
}
// --- Let Binding Support ---
// LetBinding represents a CUE let binding for local variables.
// This generates: let varName = expression
//
// Usage:
//
// tpl.AddLetBinding("resourceContent", defkit.Struct(...))
// tpl.AddLetBinding("_baseContainers", defkit.ContextOutput().Field("spec.template.spec.containers"))
type LetBinding struct {
name string
expr Value
}
// NewLetBinding creates a new let binding.
func NewLetBinding(name string, expr Value) *LetBinding {
return &LetBinding{name: name, expr: expr}
}
// Name returns the binding name.
func (l *LetBinding) Name() string { return l.name }
// Expr returns the bound expression.
func (l *LetBinding) Expr() Value { return l.expr }
// LetRef references a let binding by name.
// This generates: varName in CUE expressions.
type LetRef struct {
name string
}
func (l *LetRef) value() {}
func (l *LetRef) expr() {}
// LetVariable creates a reference to a let binding.
// Use this to reference a variable created with AddLetBinding.
//
// Example:
//
// tpl.AddLetBinding("resourceContent", defkit.Struct(...))
// // Later in template:
// defkit.LetVariable("resourceContent")
func LetVariable(name string) *LetRef {
return &LetRef{name: name}
}
// Name returns the variable name.
func (l *LetRef) Name() string { return l.name }
// --- List Comprehension with Conditionals ---
// ListComprehension represents a CUE list comprehension.
// This generates: [for v in source { ... if v.field != _|_ { field: v.field } }]
//
// Usage:
//
// defkit.ForEachIn(defkit.Param("constraints")).
// MapFields(defkit.FieldMap{
// "maxSkew": defkit.FieldRef("maxSkew"), // always included
// "minDomains": defkit.Optional("minDomains"), // only if set
// })
type ListComprehension struct {
source Value
filterCondition ListPredicate
mappings FieldMap
conditionalFields []string // fields that should only appear if set
}
func (l *ListComprehension) value() {}
func (l *ListComprehension) expr() {}
// ForEachIn creates a list comprehension from a source collection.
// This is the starting point for building list comprehensions.
//
// Example:
//
// defkit.ForEachIn(defkit.ParamRef("constraints")).
// MapFields(defkit.FieldMap{...})
func ForEachIn(source Value) *ListComprehension {
return &ListComprehension{
source: source,
conditionalFields: make([]string, 0),
}
}
// WithFilter adds a filter condition to the comprehension.
// Only items matching the filter will be included in the output.
func (l *ListComprehension) WithFilter(pred ListPredicate) *ListComprehension {
l.filterCondition = pred
return l
}
// MapFields sets the field mappings for each item in the comprehension.
// Use FieldRef for always-included fields and Optional for conditional fields.
func (l *ListComprehension) MapFields(mappings FieldMap) *ListComprehension {
l.mappings = mappings
return l
}
// WithOptionalFields marks additional fields that should only appear if set.
// This is an alternative to using Optional in the FieldMap.
func (l *ListComprehension) WithOptionalFields(fields ...string) *ListComprehension {
l.conditionalFields = append(l.conditionalFields, fields...)
return l
}
// Source returns the source value.
func (l *ListComprehension) Source() Value { return l.source }
// FilterCondition returns the filter condition.
func (l *ListComprehension) FilterCondition() ListPredicate { return l.filterCondition }
// Mappings returns the field mappings.
func (l *ListComprehension) Mappings() FieldMap { return l.mappings }
// ConditionalFields returns fields that should be conditionally included.
func (l *ListComprehension) ConditionalFields() []string { return l.conditionalFields }
// --- ListPredicate for List Comprehension Filters ---
// ListPredicate represents a condition used to filter list comprehension items.
// Unlike Predicate in collections.go (which is for runtime filtering),
// ListPredicate generates CUE conditional expressions.
type ListPredicate interface {
listPredicate()
}
// ListFieldExistsPredicate checks if a field exists (is not bottom) in list items.
type ListFieldExistsPredicate struct {
field string
}
func (p *ListFieldExistsPredicate) listPredicate() {}
// GetField returns the field name being checked.
func (p *ListFieldExistsPredicate) GetField() string { return p.field }
// ListFieldExists creates a predicate that checks if a field exists in list items.
// This generates CUE: if v.field != _|_
//
// Example:
//
// defkit.ForEachIn(source).
// WithFilter(defkit.ListFieldExists("optionalField"))
func ListFieldExists(field string) *ListFieldExistsPredicate {
return &ListFieldExistsPredicate{field: field}
}
// --- Template Extensions for Traits ---
// UsePatchContainer configures the template to use the PatchContainer pattern.
func (t *Template) UsePatchContainer(config PatchContainerConfig) *Template {
t.patchContainerConfig = &config
return t
}
// AddLetBinding adds a let binding to the template.
// Let bindings create local variables in the CUE template.
//
// Example:
//
// tpl.AddLetBinding("resourceContent", defkit.Struct(
// defkit.Field("cpu", defkit.ParamTypeString),
// defkit.Field("memory", defkit.ParamTypeString),
// ))
func (t *Template) AddLetBinding(name string, expr Value) *Template {
if t.letBindings == nil {
t.letBindings = make([]*LetBinding, 0)
}
t.letBindings = append(t.letBindings, NewLetBinding(name, expr))
return t
}
// GetLetBindings returns all let bindings.
func (t *Template) GetLetBindings() []*LetBinding {
return t.letBindings
}
// GetPatchContainerConfig returns the PatchContainer configuration.
func (t *Template) GetPatchContainerConfig() *PatchContainerConfig {
return t.patchContainerConfig
}
// SetRawPatchBlock sets a raw CUE block for the patch section.
// This allows traits to define complex patch structures directly in CUE.
func (t *Template) SetRawPatchBlock(block string) *Template {
t.rawPatchBlock = block
return t
}
// GetRawPatchBlock returns the raw patch block.
func (t *Template) GetRawPatchBlock() string {
return t.rawPatchBlock
}
// SetRawParameterBlock sets a raw CUE block for the parameter section.
// This allows traits to define complex parameter schemas directly in CUE.
func (t *Template) SetRawParameterBlock(block string) *Template {
t.rawParameterBlock = block
return t
}
// GetRawParameterBlock returns the raw parameter block.
func (t *Template) GetRawParameterBlock() string {
return t.rawParameterBlock
}
// SetRawOutputsBlock sets a raw CUE block for the outputs section.
// This allows traits to define K8s resources to create directly in CUE.
// Use this for traits that generate Services, Ingresses, HPAs, PVCs, etc.
func (t *Template) SetRawOutputsBlock(block string) *Template {
t.rawOutputsBlock = block
return t
}
// GetRawOutputsBlock returns the raw outputs block.
func (t *Template) GetRawOutputsBlock() string {
return t.rawOutputsBlock
}
// SetRawHeaderBlock sets a raw CUE block for let bindings and pre-output declarations.
// This allows traits to define local variables and helper expressions in CUE.
// Use this for let bindings, helper lists, and other declarations that come before outputs.
func (t *Template) SetRawHeaderBlock(block string) *Template {
t.rawHeaderBlock = block
return t
}
// GetRawHeaderBlock returns the raw header block.
func (t *Template) GetRawHeaderBlock() string {
return t.rawHeaderBlock
}
// ParamIsSet creates a condition that checks if a parameter is set.
// This generates CUE: parameter.name != _|_
//
// This is a convenience wrapper around IsSetCondition.
//
// Example:
//
// tpl.Patch().
// SetIf(defkit.ParamIsSet("replicas"), "spec.replicas", defkit.ParamRef("replicas"))
func ParamIsSet(name string) *IsSetCondition {
return &IsSetCondition{paramName: name}
}
// ParamNotSet creates a condition that checks if a parameter is NOT set.
// This generates CUE: parameter.name == _|_
//
// This is a convenience wrapper around NotCondition{inner: IsSetCondition}.
//
// Example:
//
// // Set default only if not explicitly specified
// tpl.Patch().
// SetIf(defkit.ParamNotSet("replicas"), "spec.replicas", defkit.Literal(1))
func ParamNotSet(name string) *NotCondition {
return &NotCondition{inner: &IsSetCondition{paramName: name}}
}
// --- ContextOutputExists condition ---
// ContextOutputExistsCondition checks if a path exists in context.output.
type ContextOutputExistsCondition struct {
baseCondition
path string
}
// Path returns the path being checked.
func (c *ContextOutputExistsCondition) Path() string { return c.path }
// ContextOutputExists creates a condition that checks if a path exists in context.output.
// This generates CUE: context.output.path != _|_
//
// Example:
//
// // Only patch if workload has spec.template (Deployment/StatefulSet)
// tpl.Patch().
// SetIf(defkit.ContextOutputExists("spec.template"), "spec.template.spec.replicas", defkit.ParamRef("replicas"))
func ContextOutputExists(path string) *ContextOutputExistsCondition {
return &ContextOutputExistsCondition{path: path}
}
// --- AllConditions compound condition ---
// AllConditionsCondition is a compound condition that requires all sub-conditions to be true.
type AllConditionsCondition struct {
baseCondition
conditions []Condition
}
// Conditions returns the list of sub-conditions.
func (a *AllConditionsCondition) Conditions() []Condition { return a.conditions }
// AllConditions creates a compound condition that requires all sub-conditions to be true.
// This generates CUE: if cond1 if cond2 if cond3 { ... }
//
// Example:
//
// // Apply only if cpu AND memory are set AND requests AND limits are NOT set
// tpl.Patch().
// SetIf(defkit.AllConditions(
// defkit.ParamIsSet("cpu"),
// defkit.ParamIsSet("memory"),
// defkit.ParamNotSet("requests"),
// defkit.ParamNotSet("limits"),
// ), "spec.resources", resourceStruct)
func AllConditions(conditions ...Condition) *AllConditionsCondition {
return &AllConditionsCondition{conditions: conditions}
}