mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 03:38:04 +00:00
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>
193 lines
4.8 KiB
Go
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)
|
|
}
|