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

477 lines
13 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 (
"strings"
"testing"
"github.com/oam-dev/kubevela/pkg/cue/definition/health"
)
func TestConditionIsTrue(t *testing.T) {
h := Health()
expr := h.Condition("Ready").IsTrue()
preamble := expr.Preamble()
if !strings.Contains(preamble, `_readyCond:`) {
t.Errorf("Expected preamble to contain _readyCond, got: %s", preamble)
}
if !strings.Contains(preamble, `c.type == "Ready"`) {
t.Errorf("Expected preamble to filter by Ready type, got: %s", preamble)
}
cue := expr.ToCUE()
if !strings.Contains(cue, `status == "True"`) {
t.Errorf("Expected ToCUE to check status == True, got: %s", cue)
}
}
func TestConditionIsFalse(t *testing.T) {
h := Health()
expr := h.Condition("Stalled").IsFalse()
cue := expr.ToCUE()
if !strings.Contains(cue, `status == "False"`) {
t.Errorf("Expected ToCUE to check status == False, got: %s", cue)
}
}
func TestConditionExists(t *testing.T) {
h := Health()
expr := h.Condition("Initialized").Exists()
cue := expr.ToCUE()
if !strings.Contains(cue, `len(_initializedCond) > 0`) {
t.Errorf("Expected ToCUE to check length > 0, got: %s", cue)
}
// Should NOT check status
if strings.Contains(cue, "status") {
t.Errorf("Exists() should not check status, got: %s", cue)
}
}
func TestConditionReasonIs(t *testing.T) {
h := Health()
expr := h.Condition("Ready").ReasonIs("Available")
cue := expr.ToCUE()
if !strings.Contains(cue, `reason == "Available"`) {
t.Errorf("Expected ToCUE to check reason, got: %s", cue)
}
}
func TestAllTrue(t *testing.T) {
h := Health()
policy := h.Policy(h.AllTrue("Ready", "Synced"))
// Should have preambles for both conditions
if !strings.Contains(policy, "_readyCond:") {
t.Errorf("Expected policy to contain _readyCond, got: %s", policy)
}
if !strings.Contains(policy, "_syncedCond:") {
t.Errorf("Expected policy to contain _syncedCond, got: %s", policy)
}
// Should combine with AND
if !strings.Contains(policy, "&&") {
t.Errorf("Expected policy to use && for AllTrue, got: %s", policy)
}
}
func TestAnyTrue(t *testing.T) {
h := Health()
policy := h.Policy(h.AnyTrue("Ready", "Available"))
// Should combine with OR
if !strings.Contains(policy, "||") {
t.Errorf("Expected policy to use || for AnyTrue, got: %s", policy)
}
}
func TestPhase(t *testing.T) {
h := Health()
expr := h.Phase("Running", "Succeeded")
if expr.Preamble() != "" {
t.Errorf("Phase should have no preamble, got: %s", expr.Preamble())
}
cue := expr.ToCUE()
if !strings.Contains(cue, `context.output.status.phase == "Running"`) {
t.Errorf("Expected ToCUE to check Running phase, got: %s", cue)
}
if !strings.Contains(cue, `context.output.status.phase == "Succeeded"`) {
t.Errorf("Expected ToCUE to check Succeeded phase, got: %s", cue)
}
if !strings.Contains(cue, "||") {
t.Errorf("Expected ToCUE to use || for multiple phases, got: %s", cue)
}
}
func TestPhaseSingle(t *testing.T) {
h := Health()
expr := h.Phase("Running")
cue := expr.ToCUE()
// Single phase should not have ||
if strings.Contains(cue, "||") {
t.Errorf("Single phase should not use ||, got: %s", cue)
}
if cue != `context.output.status.phase == "Running"` {
t.Errorf("Unexpected CUE for single phase: %s", cue)
}
}
func TestPhaseField(t *testing.T) {
h := Health()
expr := h.PhaseField("status.currentPhase", "Active")
cue := expr.ToCUE()
if !strings.Contains(cue, "context.output.status.currentPhase") {
t.Errorf("Expected custom path, got: %s", cue)
}
}
func TestFieldEq(t *testing.T) {
h := Health()
expr := h.Field("status.state").Eq("active")
cue := expr.ToCUE()
expected := `context.output.status.state == "active"`
if cue != expected {
t.Errorf("Expected %s, got: %s", expected, cue)
}
}
func TestFieldGt(t *testing.T) {
h := Health()
expr := h.Field("status.replicas").Gt(0)
cue := expr.ToCUE()
expected := `context.output.status.replicas > 0`
if cue != expected {
t.Errorf("Expected %s, got: %s", expected, cue)
}
}
func TestFieldGte(t *testing.T) {
h := Health()
expr := h.Field("status.availableReplicas").Gte(1)
cue := expr.ToCUE()
expected := `context.output.status.availableReplicas >= 1`
if cue != expected {
t.Errorf("Expected %s, got: %s", expected, cue)
}
}
func TestFieldLt(t *testing.T) {
h := Health()
expr := h.Field("status.failedReplicas").Lt(5)
cue := expr.ToCUE()
expected := `context.output.status.failedReplicas < 5`
if cue != expected {
t.Errorf("Expected %s, got: %s", expected, cue)
}
}
func TestFieldIn(t *testing.T) {
h := Health()
expr := h.Field("status.phase").In("Running", "Succeeded", "Complete")
cue := expr.ToCUE()
if !strings.Contains(cue, `== "Running"`) {
t.Errorf("Expected Running in In(), got: %s", cue)
}
if !strings.Contains(cue, `== "Succeeded"`) {
t.Errorf("Expected Succeeded in In(), got: %s", cue)
}
if !strings.Contains(cue, "||") {
t.Errorf("Expected || in In(), got: %s", cue)
}
}
func TestFieldRef(t *testing.T) {
h := Health()
expr := h.Field("status.readyReplicas").Eq(h.FieldRef("spec.replicas"))
cue := expr.ToCUE()
expected := `context.output.status.readyReplicas == context.output.spec.replicas`
if cue != expected {
t.Errorf("Expected %s, got: %s", expected, cue)
}
}
func TestExists(t *testing.T) {
h := Health()
expr := h.Exists("status.loadBalancer.ingress")
cue := expr.ToCUE()
expected := `context.output.status.loadBalancer.ingress != _|_`
if cue != expected {
t.Errorf("Expected %s, got: %s", expected, cue)
}
}
func TestNotExists(t *testing.T) {
h := Health()
expr := h.NotExists("status.error")
cue := expr.ToCUE()
expected := `context.output.status.error == _|_`
if cue != expected {
t.Errorf("Expected %s, got: %s", expected, cue)
}
}
func TestAnd(t *testing.T) {
h := Health()
expr := h.And(
h.Field("status.replicas").Gt(0),
h.Exists("status.endpoint"),
)
cue := expr.ToCUE()
if !strings.Contains(cue, "&&") {
t.Errorf("Expected && in And(), got: %s", cue)
}
if !strings.Contains(cue, "status.replicas > 0") {
t.Errorf("Expected first expression in And(), got: %s", cue)
}
if !strings.Contains(cue, "status.endpoint != _|_") {
t.Errorf("Expected second expression in And(), got: %s", cue)
}
}
func TestOr(t *testing.T) {
h := Health()
expr := h.Or(
h.Phase("Running"),
h.Phase("Succeeded"),
)
cue := expr.ToCUE()
if !strings.Contains(cue, "||") {
t.Errorf("Expected || in Or(), got: %s", cue)
}
}
func TestNot(t *testing.T) {
h := Health()
expr := h.Not(h.Condition("Stalled").IsTrue())
cue := expr.ToCUE()
if !strings.HasPrefix(cue, "!(") {
t.Errorf("Expected Not() to wrap with !(), got: %s", cue)
}
}
func TestAlways(t *testing.T) {
h := Health()
expr := h.Always()
if expr.Preamble() != "" {
t.Errorf("Always should have no preamble, got: %s", expr.Preamble())
}
if expr.ToCUE() != "true" {
t.Errorf("Always should return true, got: %s", expr.ToCUE())
}
}
func TestHealthPolicy(t *testing.T) {
h := Health()
policy := h.Policy(h.Condition("Ready").IsTrue())
if !strings.Contains(policy, "isHealth:") {
t.Errorf("Expected isHealth: in policy, got: %s", policy)
}
if !strings.Contains(policy, "_readyCond:") {
t.Errorf("Expected preamble in policy, got: %s", policy)
}
}
func TestHealthPolicyNoPreamble(t *testing.T) {
h := Health()
policy := h.Policy(h.Always())
expected := "isHealth: true"
if policy != expected {
t.Errorf("Expected %s, got: %s", expected, policy)
}
}
func TestComplexComposition(t *testing.T) {
h := Health()
// Real-world example: Crossplane-style + field check
expr := h.And(
h.Condition("Ready").IsTrue(),
h.Not(h.Condition("Stalled").IsTrue()),
h.Or(
h.Field("status.replicas").Gte(1),
h.Exists("status.endpoint"),
),
)
policy := h.Policy(expr)
// Should have all preambles
if !strings.Contains(policy, "_readyCond:") {
t.Errorf("Missing _readyCond preamble")
}
if !strings.Contains(policy, "_stalledCond:") {
t.Errorf("Missing _stalledCond preamble")
}
// Should have complex expression
if !strings.Contains(policy, "isHealth:") {
t.Errorf("Missing isHealth:")
}
if !strings.Contains(policy, "&&") {
t.Errorf("Missing && combinator")
}
if !strings.Contains(policy, "||") {
t.Errorf("Missing || combinator")
}
if !strings.Contains(policy, "!(") {
t.Errorf("Missing Not() expression")
}
}
// TestConditionExprHandlesMissingStatus reproduces the fixtures from the
// GitHub issue: a health policy built with AllTrue must evaluate to false
// (not a CUE evaluation error) when status or status.conditions is absent,
// and must skip condition entries that have no type instead of erroring.
func TestConditionExprHandlesMissingStatus(t *testing.T) {
h := Health()
policy := h.Policy(h.And(
h.Exists("status"),
h.Exists("status.conditions"),
h.AllTrue("Ready", "Synced"),
))
cases := []struct {
name string
output map[string]interface{}
want bool
}{
{
name: "no status",
output: map[string]interface{}{},
want: false,
},
{
name: "empty status",
output: map[string]interface{}{"status": map[string]interface{}{}},
want: false,
},
{
name: "ready and synced",
output: map[string]interface{}{"status": map[string]interface{}{"conditions": []interface{}{
map[string]interface{}{"type": "Ready", "status": "True"},
map[string]interface{}{"type": "Synced", "status": "True"},
}}},
want: true,
},
{
name: "ready false",
output: map[string]interface{}{"status": map[string]interface{}{"conditions": []interface{}{
map[string]interface{}{"type": "Ready", "status": "False"},
map[string]interface{}{"type": "Synced", "status": "True"},
}}},
want: false,
},
{
name: "condition entry without type",
output: map[string]interface{}{"status": map[string]interface{}{"conditions": []interface{}{
map[string]interface{}{"type": "Ready", "status": "True"},
map[string]interface{}{"type": "Synced", "status": "True"},
map[string]interface{}{"status": "True"},
}}},
want: true,
},
{
name: "matched condition without status",
output: map[string]interface{}{"status": map[string]interface{}{"conditions": []interface{}{
map[string]interface{}{"type": "Ready"},
map[string]interface{}{"type": "Synced", "status": "True"},
}}},
want: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
templateContext := map[string]interface{}{"output": c.output}
got, err := health.CheckHealth(templateContext, policy, nil)
if err != nil {
t.Fatalf("CheckHealth returned an error instead of a health verdict: %v", err)
}
if got != c.want {
t.Errorf("expected healthy=%v, got %v", c.want, got)
}
})
}
}
func TestFieldContains(t *testing.T) {
h := Health()
expr := h.Field("status.message").Contains("ready")
cue := expr.ToCUE()
expected := `strings.Contains(context.output.status.message, "ready")`
if cue != expected {
t.Errorf("Expected %s, got: %s", expected, cue)
}
}
func TestComponentHealthPolicyExpr(t *testing.T) {
h := Health()
// Test that HealthPolicyExpr correctly integrates with ComponentDefinition
comp := NewComponent("test-component").
Description("A test component").
HealthPolicyExpr(h.Condition("Ready").IsTrue())
policy := comp.GetHealthPolicy()
// Should contain the preamble and isHealth expression
if !strings.Contains(policy, "_readyCond:") {
t.Errorf("Expected policy to contain _readyCond preamble, got: %s", policy)
}
if !strings.Contains(policy, "isHealth:") {
t.Errorf("Expected policy to contain isHealth:, got: %s", policy)
}
if !strings.Contains(policy, `status == "True"`) {
t.Errorf("Expected policy to check status == True, got: %s", policy)
}
}
func TestComponentHealthPolicyExprComplex(t *testing.T) {
h := Health()
// Test complex health expression with ComponentDefinition
comp := NewComponent("crossplane-resource").
HealthPolicyExpr(h.And(
h.Condition("Ready").IsTrue(),
h.Not(h.Condition("Stalled").IsTrue()),
))
policy := comp.GetHealthPolicy()
// Should have both condition preambles
if !strings.Contains(policy, "_readyCond:") {
t.Errorf("Expected _readyCond preamble, got: %s", policy)
}
if !strings.Contains(policy, "_stalledCond:") {
t.Errorf("Expected _stalledCond preamble, got: %s", policy)
}
// Should have AND and NOT
if !strings.Contains(policy, "&&") {
t.Errorf("Expected && in policy, got: %s", policy)
}
if !strings.Contains(policy, "!(") {
t.Errorf("Expected !( in policy, got: %s", policy)
}
}