Files
helm-dashboard/pkg/dashboard/handlers/kubeHandlers.go
Andrei Pohilko 4fb2eb099a fix: use apiVersion to disambiguate CRDs with same kind (#504)
When multiple CRDs share the same kind but different API groups
(e.g. Traefik's Middleware under traefik.io and traefik.containo.us),
the dashboard failed to look up the correct resource. Thread apiVersion
through the resource fetch chain and use group-qualified kind
(e.g. Widget.new.example.com) for kubectl lookups.

Closes #504

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:55:48 +00:00

193 lines
4.8 KiB
Go

package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/v2/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/errors"
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"k8s.io/utils/strings/slices"
)
const Unknown = "Unknown"
const Healthy = "Healthy"
const Unhealthy = "Unhealthy"
const Progressing = "Progressing"
type KubeHandler struct {
*Contexted
}
func (h *KubeHandler) GetContexts(c *gin.Context) {
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
res, err := h.Data.ListContexts()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, res)
}
func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
qp, err := utils.GetQueryProps(c)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
kind := utils.QualifiedKind(c.Param("kind"), qp.APIVersion)
res, err := app.K8s.GetResourceInfo(kind, qp.Namespace, qp.Name)
if errors.IsNotFound(err) {
res = &v12.Carp{Status: v12.CarpStatus{Phase: "NotFound", Message: err.Error()}}
//_ = c.AbortWithError(http.StatusNotFound, err)
//return
} else if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
res.Status = *EnhanceStatus(res, nil)
c.IndentedJSON(http.StatusOK, res)
}
func EnhanceStatus(res *v12.Carp, err error) *v12.CarpStatus {
s := res.Status
if s.Conditions == nil {
s.Conditions = []v12.CarpCondition{}
}
c := v12.CarpCondition{
Type: "hdHealth",
Status: Unknown,
Reason: s.Reason,
Message: s.Message,
}
// custom logic to provide most meaningful status for the resource
if err != nil {
c.Reason = "ErrorGettingStatus"
c.Message = err.Error()
} else if s.Phase == "Error" {
c.Status = Unhealthy
} else if slices.Contains([]string{"Available", "Active", "Established", "Bound", "Ready"}, string(s.Phase)) {
c.Status = Healthy
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" || s.Phase == "Terminating" {
c.Status = Progressing
c.Reason = string(s.Phase)
} else if s.Phase == "" {
c.Status = Healthy
c.Reason = "Exists"
} else {
log.Warnf("Unhandled status: %v", s)
c.Reason = string(s.Phase)
}
s.Conditions = append(s.Conditions, c)
return &s
}
func applyHealthFromCondition(c *v12.CarpCondition, cond v12.CarpCondition) {
if cond.Status == "False" {
c.Status = Unhealthy
} else {
c.Status = Healthy
}
c.Reason = cond.Reason
c.Message = cond.Message
}
func applyCustomConditions(s *v12.CarpStatus, c *v12.CarpCondition) {
for _, cond := range s.Conditions {
switch cond.Type {
case "Progressing": // https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
if cond.Status == "False" {
c.Status = Unhealthy
c.Reason = cond.Reason
c.Message = cond.Message
} else if cond.Reason != "NewReplicaSetAvailable" {
c.Status = Progressing
c.Reason = cond.Reason
c.Message = cond.Message
}
case "Complete": // condition for Job
if c.Status == Unknown && cond.Status == "True" {
c.Status = Healthy
c.Reason = cond.Reason
c.Message = cond.Message
}
case "Failed": // condition for Job
if c.Status == Unknown && cond.Status == "True" {
c.Status = Unhealthy
c.Reason = cond.Reason
c.Message = cond.Message
}
case "Available", "DisruptionAllowed", "Ready", "AbleToScale", "ScalingActive":
if c.Status == Unknown {
applyHealthFromCondition(c, cond)
}
case "Established", "NamesAccepted": // condition for CRD
if c.Status == Unknown || c.Status == Healthy {
applyHealthFromCondition(c, cond)
}
}
}
}
func (h *KubeHandler) Describe(c *gin.Context) {
qp, err := utils.GetQueryProps(c)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
kind := utils.QualifiedKind(c.Param("kind"), qp.APIVersion)
res, err := app.K8s.DescribeResource(kind, qp.Namespace, qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, res)
}
func (h *KubeHandler) GetNameSpaces(c *gin.Context) {
if c.Param("kind") != "namespaces" {
_ = c.AbortWithError(http.StatusBadRequest, errorx.AssertionFailed.New("Only 'namespaces' kind is allowed for listing"))
return
}
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
res, err := app.K8s.GetNameSpaces()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, res)
}