From f857f8dfdce5af34635d32eda849f4e1a5e1c086 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Tue, 17 Mar 2026 02:33:33 -0700 Subject: [PATCH] feat(health): add health status for ExternalSecret, Job, HPA, and Namespace (#662) Add condition-based health status calculation for additional resource kinds: - ExternalSecret: checks "Ready" condition - Job: checks "Complete" and "Failed" conditions - HorizontalPodAutoscaler: checks "AbleToScale" and "ScalingActive" conditions - Namespace: handles "Terminating" phase as Progressing Closes #418 Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- pkg/dashboard/handlers/kubeHandlers.go | 30 +++- pkg/dashboard/handlers/kubeHandlers_test.go | 156 ++++++++++++++++++++ 2 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 pkg/dashboard/handlers/kubeHandlers_test.go diff --git a/pkg/dashboard/handlers/kubeHandlers.go b/pkg/dashboard/handlers/kubeHandlers.go index 2b2c7fb..2837244 100644 --- a/pkg/dashboard/handlers/kubeHandlers.go +++ b/pkg/dashboard/handlers/kubeHandlers.go @@ -86,7 +86,7 @@ func EnhanceStatus(res *v12.Carp, err error) *v12.CarpStatus { c.Reason = "Exists" //since there is no condition to check here, we can set reason as exists. } else if s.Phase == "" && len(s.Conditions) > 0 { applyCustomConditions(&s, &c) - } else if s.Phase == "Pending" { + } else if s.Phase == "Pending" || s.Phase == "Terminating" { c.Status = Progressing c.Reason = string(s.Phase) } else if s.Phase == "" { @@ -137,6 +137,34 @@ func applyCustomConditions(s *v12.CarpStatus, c *v12.CarpCondition) { } c.Reason = cond.Reason c.Message = cond.Message + } else if cond.Type == "Ready" && c.Status == Unknown { // condition for ExternalSecret + if cond.Status == "False" { + c.Status = Unhealthy + } else { + c.Status = Healthy + } + c.Reason = cond.Reason + c.Message = cond.Message + } else if cond.Type == "Complete" && c.Status == Unknown { // condition for Job + if cond.Status == "True" { + c.Status = Healthy + c.Reason = cond.Reason + c.Message = cond.Message + } + } else if cond.Type == "Failed" && c.Status == Unknown { // condition for Job + if cond.Status == "True" { + c.Status = Unhealthy + c.Reason = cond.Reason + c.Message = cond.Message + } + } else if (cond.Type == "AbleToScale" || cond.Type == "ScalingActive") && c.Status == Unknown { // condition for HorizontalPodAutoscaler + if cond.Status == "False" { + c.Status = Unhealthy + } else { + c.Status = Healthy + } + c.Reason = cond.Reason + c.Message = cond.Message } } } diff --git a/pkg/dashboard/handlers/kubeHandlers_test.go b/pkg/dashboard/handlers/kubeHandlers_test.go new file mode 100644 index 0000000..c5bb182 --- /dev/null +++ b/pkg/dashboard/handlers/kubeHandlers_test.go @@ -0,0 +1,156 @@ +package handlers + +import ( + "testing" + + v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1" +) + +func TestEnhanceStatus_ExternalSecret_Ready(t *testing.T) { + res := &v1.Carp{ + Status: v1.CarpStatus{ + Conditions: []v1.CarpCondition{ + {Type: "Ready", Status: "True", Reason: "SecretSynced", Message: "Secret was synced"}, + }, + }, + } + s := EnhanceStatus(res, nil) + hdCond := findHDHealth(s) + if hdCond == nil { + t.Fatal("expected hdHealth condition") + } + if hdCond.Status != Healthy { + t.Errorf("expected Healthy, got %s", hdCond.Status) + } +} + +func TestEnhanceStatus_ExternalSecret_NotReady(t *testing.T) { + res := &v1.Carp{ + Status: v1.CarpStatus{ + Conditions: []v1.CarpCondition{ + {Type: "Ready", Status: "False", Reason: "SecretSyncError", Message: "could not sync secret"}, + }, + }, + } + s := EnhanceStatus(res, nil) + hdCond := findHDHealth(s) + if hdCond == nil { + t.Fatal("expected hdHealth condition") + } + if hdCond.Status != Unhealthy { + t.Errorf("expected Unhealthy, got %s", hdCond.Status) + } +} + +func TestEnhanceStatus_Job_Complete(t *testing.T) { + res := &v1.Carp{ + Status: v1.CarpStatus{ + Conditions: []v1.CarpCondition{ + {Type: "Complete", Status: "True", Reason: "JobComplete", Message: "Job completed"}, + }, + }, + } + s := EnhanceStatus(res, nil) + hdCond := findHDHealth(s) + if hdCond == nil { + t.Fatal("expected hdHealth condition") + } + if hdCond.Status != Healthy { + t.Errorf("expected Healthy, got %s", hdCond.Status) + } +} + +func TestEnhanceStatus_Job_Failed(t *testing.T) { + res := &v1.Carp{ + Status: v1.CarpStatus{ + Conditions: []v1.CarpCondition{ + {Type: "Failed", Status: "True", Reason: "BackoffLimitExceeded", Message: "Job has reached the specified backoff limit"}, + }, + }, + } + s := EnhanceStatus(res, nil) + hdCond := findHDHealth(s) + if hdCond == nil { + t.Fatal("expected hdHealth condition") + } + if hdCond.Status != Unhealthy { + t.Errorf("expected Unhealthy, got %s", hdCond.Status) + } +} + +func TestEnhanceStatus_HPA_AbleToScale(t *testing.T) { + res := &v1.Carp{ + Status: v1.CarpStatus{ + Conditions: []v1.CarpCondition{ + {Type: "AbleToScale", Status: "True", Reason: "ReadyForNewScale", Message: "recommended size matches current size"}, + }, + }, + } + s := EnhanceStatus(res, nil) + hdCond := findHDHealth(s) + if hdCond == nil { + t.Fatal("expected hdHealth condition") + } + if hdCond.Status != Healthy { + t.Errorf("expected Healthy, got %s", hdCond.Status) + } +} + +func TestEnhanceStatus_HPA_UnableToScale(t *testing.T) { + res := &v1.Carp{ + Status: v1.CarpStatus{ + Conditions: []v1.CarpCondition{ + {Type: "AbleToScale", Status: "False", Reason: "FailedGetScale", Message: "the HPA controller was unable to get the target's current scale"}, + }, + }, + } + s := EnhanceStatus(res, nil) + hdCond := findHDHealth(s) + if hdCond == nil { + t.Fatal("expected hdHealth condition") + } + if hdCond.Status != Unhealthy { + t.Errorf("expected Unhealthy, got %s", hdCond.Status) + } +} + +func TestEnhanceStatus_Namespace_Active(t *testing.T) { + res := &v1.Carp{ + Status: v1.CarpStatus{ + Phase: "Active", + }, + } + s := EnhanceStatus(res, nil) + hdCond := findHDHealth(s) + if hdCond == nil { + t.Fatal("expected hdHealth condition") + } + if hdCond.Status != Healthy { + t.Errorf("expected Healthy, got %s", hdCond.Status) + } +} + +func TestEnhanceStatus_Namespace_Terminating(t *testing.T) { + res := &v1.Carp{ + Status: v1.CarpStatus{ + Phase: "Terminating", + }, + } + s := EnhanceStatus(res, nil) + hdCond := findHDHealth(s) + if hdCond == nil { + t.Fatal("expected hdHealth condition") + } + if hdCond.Status != Progressing { + t.Errorf("expected Progressing, got %s", hdCond.Status) + } +} + +func findHDHealth(s *v1.CarpStatus) *v1.CarpCondition { + for i, c := range s.Conditions { + if c.Type == "hdHealth" { + return &s.Conditions[i] + } + } + return nil +}