Files
544fc6042e Fix: defkit health conditions erroring when status is absent (#7299)
* Fix: defkit health conditions erroring when status is absent

Motivation:
defkit.ConditionExpr (used by HealthPolicyExpr/AllTrue) builds a CUE
comprehension over context.output.status.conditions. When status or
status.conditions doesn't exist yet - e.g. during the first few reconciles
after a Crossplane claim is created - that comprehension source is CUE
bottom (_|_). CUE's && does not short-circuit, so the bottom propagates
through the whole isHealth expression even past Exists() guards meant to
protect it. A condition entry without a type field hit the same problem.
Separately, ToCUE() indexed the filtered list at [0], which also errors on
an empty list regardless of a preceding len(...) > 0 check, for the same
non-short-circuit reason.

Approach:
- Preamble() now sources the comprehension from
  *context.output.status.conditions | []. CUE disjunctions discard any
  operand that evaluates to bottom, so this falls back to an empty list
  instead of erroring when status/conditions are absent, while still
  ranging over the real list when it exists. `if c.type != _|_` skips
  condition entries missing a type field instead of letting the
  comparison go bottom.
- ToCUE() now checks status/reason by filtering the condition list with
  len([for c in ... if ...]) > 0 instead of indexing [0], so there's
  never an out-of-range index to evaluate.

Testing:
Added TestConditionExprHandlesMissingStatus in
pkg/definition/defkit/health_expr_test.go, which builds a policy with
h.And(h.Exists("status"), h.Exists("status.conditions"),
h.AllTrue("Ready","Synced")) and runs it through health.CheckHealth
against five fixtures: no status, empty status, Ready+Synced true, Ready
false, and a condition entry missing type.

```release-note
Fixed `defkit` health policies built with `ConditionExpr`/`AllTrue` erroring
instead of evaluating to unhealthy when the target resource has no `status` or
`status.conditions` yet.
```

Report: https://github.com/kubevela/kubevela/issues/7284
Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>

* Fix: guard c.status/c.reason against bottom in defkit condition ToCUE

A condition entry with a matching type but no status field (e.g. a
transient/partial condition) made the CUE comparison c.status == "..."
evaluate to bottom instead of false, causing a hard evaluation error
rather than an unhealthy verdict. Guard c.status and c.reason with
!= _|_ before comparing, mirroring the existing c.type guard in
Preamble().

Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>

---------

Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
Co-authored-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
2026-08-12 11:31:23 +01:00

367 lines
9.8 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"
"strings"
)
// HealthExpression is the composable unit for building health policies.
// All health primitives implement this interface and can be combined
// using Health().And(), Health().Or(), and Health().Not().
type HealthExpression interface {
// ToCUE generates the CUE expression for this health check.
// The expression should evaluate to a boolean.
ToCUE() string
// Preamble returns any CUE definitions needed before the isHealth expression.
// For example, condition checks need helper variables to extract conditions.
Preamble() string
}
// HealthPolicy wraps a HealthExpression and generates the complete healthPolicy CUE block.
func HealthPolicy(expr HealthExpression) string {
preamble := expr.Preamble()
if preamble != "" {
return preamble + "\nisHealth: " + expr.ToCUE()
}
return "isHealth: " + expr.ToCUE()
}
// --- Condition Expressions ---
// ConditionExpr checks a condition in status.conditions[] array.
type ConditionExpr struct {
condType string
expectedStatus string
expectedReason string
checkExists bool
}
// IsTrue checks if the condition status is "True".
func (c *ConditionExpr) IsTrue() HealthExpression {
c.expectedStatus = "True"
return c
}
// IsFalse checks if the condition status is "False".
func (c *ConditionExpr) IsFalse() HealthExpression {
c.expectedStatus = "False"
return c
}
// Is checks if the condition status matches the expected value.
func (c *ConditionExpr) Is(status string) HealthExpression {
c.expectedStatus = status
return c
}
// Exists checks if the condition exists (regardless of status).
func (c *ConditionExpr) Exists() HealthExpression {
c.checkExists = true
return c
}
// ReasonIs checks if the condition has a specific reason.
func (c *ConditionExpr) ReasonIs(reason string) HealthExpression {
c.expectedReason = reason
return c
}
func (c *ConditionExpr) varName() string {
return "_" + strings.ToLower(c.condType) + "Cond"
}
func (c *ConditionExpr) Preamble() string {
varName := c.varName()
// context.output.status.conditions may not exist yet (e.g. right after creation),
// which would make the comprehension source bottom. The `*X | []` disjunction
// discards that bottom disjunct and falls back to an empty list instead of
// propagating the error. c.type is filtered defensively for the same reason:
// a condition entry without a type would otherwise make the comparison bottom.
return fmt.Sprintf(`%s: [ for c in *context.output.status.conditions | [] if c.type != _|_ if c.type == "%s" { c } ]`,
varName, c.condType)
}
func (c *ConditionExpr) ToCUE() string {
varName := c.varName()
if c.checkExists {
return fmt.Sprintf("len(%s) > 0", varName)
}
// Filter rather than index %s[0]: && does not short-circuit in CUE, so an
// out-of-range index on an empty list would propagate as bottom even when
// len(%s) > 0 is false. c.status/c.reason are guarded with != _|_ for the
// same reason c.type is guarded in Preamble: a condition entry missing
// that field would otherwise make the comparison bottom instead of false.
if c.expectedReason != "" {
return fmt.Sprintf(`len([ for c in %s if c.status != _|_ if c.status == "%s" if c.reason != _|_ if c.reason == "%s" { c } ]) > 0`,
varName, c.expectedStatus, c.expectedReason)
}
return fmt.Sprintf(`len([ for c in %s if c.status != _|_ if c.status == "%s" { c } ]) > 0`,
varName, c.expectedStatus)
}
// --- Phase Expressions ---
// phaseExpr checks the status.phase field.
type phaseExpr struct {
fieldPath string
phases []string
}
func (p *phaseExpr) Preamble() string {
return ""
}
func (p *phaseExpr) ToCUE() string {
if len(p.phases) == 1 {
return fmt.Sprintf(`%s == "%s"`, p.fieldPath, p.phases[0])
}
parts := make([]string, len(p.phases))
for i, phase := range p.phases {
parts[i] = fmt.Sprintf(`%s == "%s"`, p.fieldPath, phase)
}
return strings.Join(parts, " || ")
}
// --- Field Expressions ---
// HealthFieldExpr provides comparison operations on a status field.
type HealthFieldExpr struct {
path string
}
// Eq checks if the field equals the given value.
func (f *HealthFieldExpr) Eq(value any) HealthExpression {
return &fieldCompareExpr{path: f.path, op: "==", value: value}
}
// Ne checks if the field does not equal the given value.
func (f *HealthFieldExpr) Ne(value any) HealthExpression {
return &fieldCompareExpr{path: f.path, op: "!=", value: value}
}
// Gt checks if the field is greater than the given value.
func (f *HealthFieldExpr) Gt(value any) HealthExpression {
return &fieldCompareExpr{path: f.path, op: ">", value: value}
}
// Gte checks if the field is greater than or equal to the given value.
func (f *HealthFieldExpr) Gte(value any) HealthExpression {
return &fieldCompareExpr{path: f.path, op: ">=", value: value}
}
// Lt checks if the field is less than the given value.
func (f *HealthFieldExpr) Lt(value any) HealthExpression {
return &fieldCompareExpr{path: f.path, op: "<", value: value}
}
// Lte checks if the field is less than or equal to the given value.
func (f *HealthFieldExpr) Lte(value any) HealthExpression {
return &fieldCompareExpr{path: f.path, op: "<=", value: value}
}
// In checks if the field value is one of the given values.
func (f *HealthFieldExpr) In(values ...any) HealthExpression {
return &fieldInExpr{path: f.path, values: values}
}
// Contains checks if the string field contains the given substring.
func (f *HealthFieldExpr) Contains(substr string) HealthExpression {
return &fieldContainsExpr{path: f.path, substr: substr}
}
// fieldCompareExpr is a field comparison expression.
type fieldCompareExpr struct {
path string
op string
value any
}
func (f *fieldCompareExpr) Preamble() string {
return ""
}
func (f *fieldCompareExpr) ToCUE() string {
fullPath := "context.output." + f.path
// Check if value is a HealthFieldRefExpr
if ref, ok := f.value.(*HealthFieldRefExpr); ok {
return fmt.Sprintf("%s %s %s", fullPath, f.op, ref.ToCUE())
}
return fmt.Sprintf("%s %s %s", fullPath, f.op, formatValue(f.value))
}
// fieldInExpr checks if a field is in a set of values.
type fieldInExpr struct {
path string
values []any
}
func (f *fieldInExpr) Preamble() string {
return ""
}
func (f *fieldInExpr) ToCUE() string {
fullPath := "context.output." + f.path
parts := make([]string, len(f.values))
for i, v := range f.values {
parts[i] = fmt.Sprintf("%s == %s", fullPath, formatValue(v))
}
return strings.Join(parts, " || ")
}
// fieldContainsExpr checks if a string field contains a substring.
type fieldContainsExpr struct {
path string
substr string
}
func (f *fieldContainsExpr) Preamble() string {
return ""
}
func (f *fieldContainsExpr) ToCUE() string {
fullPath := "context.output." + f.path
return fmt.Sprintf(`strings.Contains(%s, %s)`, fullPath, formatValue(f.substr))
}
// --- FieldRef for field-to-field comparisons ---
// HealthFieldRefExpr represents a reference to another field (for comparisons).
type HealthFieldRefExpr struct {
path string
}
func (f *HealthFieldRefExpr) Preamble() string {
return ""
}
func (f *HealthFieldRefExpr) ToCUE() string {
return "context.output." + f.path
}
// --- Exists / NotExists ---
// existsExpr checks if a field exists (is not bottom _|_).
type existsExpr struct {
path string
negate bool
}
func (e *existsExpr) Preamble() string {
return ""
}
func (e *existsExpr) ToCUE() string {
fullPath := "context.output." + e.path
if e.negate {
return fmt.Sprintf("%s == _|_", fullPath)
}
return fmt.Sprintf("%s != _|_", fullPath)
}
// --- Combinators: And, Or, Not ---
// andExpr combines multiple expressions with AND.
type andExpr struct {
exprs []HealthExpression
}
func (a *andExpr) Preamble() string {
var preambles []string
for _, expr := range a.exprs {
if p := expr.Preamble(); p != "" {
preambles = append(preambles, p)
}
}
return strings.Join(preambles, "\n")
}
func (a *andExpr) ToCUE() string {
parts := make([]string, len(a.exprs))
for i, expr := range a.exprs {
parts[i] = "(" + expr.ToCUE() + ")"
}
return strings.Join(parts, " && ")
}
// orExpr combines multiple expressions with OR.
type orExpr struct {
exprs []HealthExpression
}
func (o *orExpr) Preamble() string {
var preambles []string
for _, expr := range o.exprs {
if p := expr.Preamble(); p != "" {
preambles = append(preambles, p)
}
}
return strings.Join(preambles, "\n")
}
func (o *orExpr) ToCUE() string {
parts := make([]string, len(o.exprs))
for i, expr := range o.exprs {
parts[i] = "(" + expr.ToCUE() + ")"
}
return strings.Join(parts, " || ")
}
// notExpr negates an expression.
type notExpr struct {
expr HealthExpression
}
func (n *notExpr) Preamble() string {
return n.expr.Preamble()
}
func (n *notExpr) ToCUE() string {
return "!(" + n.expr.ToCUE() + ")"
}
// --- Always (existence-based health) ---
// alwaysExpr always returns true (resource existence = healthy).
type alwaysExpr struct{}
func (a *alwaysExpr) Preamble() string {
return ""
}
func (a *alwaysExpr) ToCUE() string {
return "true"
}
// --- Helper functions ---
// formatValue formats a Go value for CUE output.
func formatValue(v any) string {
switch val := v.(type) {
case string:
return fmt.Sprintf("%q", val) // %q properly escapes quotes and special chars
case int, int32, int64, float32, float64:
return fmt.Sprintf("%v", val)
case bool:
return fmt.Sprintf("%t", val)
default:
return fmt.Sprintf("%v", val)
}
}