Files
kubevela/pkg/definition/defkit/patch_container.go
Ayush Kumar 012a134829 Feat: extend fluent builder API validator patterns (#7092)
* Feat: Add NotEmpty and NegativePattern constraints to StringParam; implement Closed for MapParam

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

* feat: add validation support for array and map parameters

- Introduced validators for ArrayParam and MapParam, allowing for cross-field validation within structured parameters.
- Added NonEmpty validation for ArrayParam to ensure arrays are not empty.
- Implemented ConditionalStructOp for conditional struct generation based on specified conditions.
- Created a new Validator type for defining validation rules with optional guard conditions.
- Added tests for various validation scenarios, including mutual exclusion and conditional parameters.
- Enhanced the CUE generation logic to incorporate new validation features and conditional struct handling.

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

* feat: extend fluent API with new scoped field conditions and improve validation checks

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

* feat: enhance ArrayParam with NotEmpty constraint and update ScopedField documentation

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

* feat: rename ScopedField to LocalField for improved clarity in condition building

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

* feat: refactor local field conditions to use RegexMatch and streamline condition building

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

* feat: simplify condition handling by removing unused comparison types and refactoring NotCondition usage

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

* refactor: remove unused raw CUE block handling from baseDefinition and ComponentDefinition

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

* test: update condition handling in parameter tests to use NotExpr and Cond methods

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

* refactor: remove negative pattern handling from StringParam and related tests

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

* feat: add support for emitting raw header blocks in template generation

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

* refactor: remove non-empty check from ArrayParam and update related tests

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

* refactor: convert parameter constraint tests to use Ginkgo and Gomega for improved readability and maintainability

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

* feat: extend fluent APIs for OAM with new CUE generation tests and condition evaluations

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

* refactor: clean up whitespace in component, cuegen, expr, param, and resource files

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

* feat: enhance CUE generation by adding support for new expression types and iterator references

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

* refactor: remove unnecessary whitespace in cuegen.go

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

* refactor: rename LenOf to LenOfExpr for clarity in comparison methods

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

* feat: enhance CUE generation and validation for string arrays in ArrayParam

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

* ci: retrigger checks

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

---------

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Signed-off-by: Ayush Kumar <aykumar@guidewire.com>
Co-authored-by: Ayush Kumar <aykumar@guidewire.com>
2026-04-09 14:25:06 +01: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 Not(IsSetCondition).
//
// Example:
//
// // Set default only if not explicitly specified
// tpl.Patch().
// SetIf(defkit.ParamNotSet("replicas"), "spec.replicas", defkit.Literal(1))
func ParamNotSet(name string) *NotExpr {
return Not(&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}
}