diff --git a/pkg/dashboard/handlers/helmHandlers.go b/pkg/dashboard/handlers/helmHandlers.go
index 79ff5b5..1607ce9 100644
--- a/pkg/dashboard/handlers/helmHandlers.go
+++ b/pkg/dashboard/handlers/helmHandlers.go
@@ -136,6 +136,26 @@ func (h *HelmHandler) Resources(c *gin.Context) {
return
}
+ if c.Query("health") != "" { // we need to query k8s for health status
+ app := h.GetApp(c)
+ if app == nil {
+ _ = c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+ for _, obj := range res {
+ ns := obj.Namespace
+ if ns == "" {
+ ns = c.Param("ns")
+ }
+ info, err := app.K8s.GetResourceInfo(obj.Kind, ns, obj.Name)
+ if err != nil {
+ _ = c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+ obj.Status = *EnhanceStatus(info)
+ }
+ }
+
c.IndentedJSON(http.StatusOK, res)
}
@@ -587,23 +607,24 @@ func (h *HelmHandler) repoFromArtifactHub(name string) (string, error) {
return true
}
+ // more popular
+ if ri.Stars != rj.Stars {
+ return ri.Stars > rj.Stars
+ }
+
// or from verified publishers
if ri.Repository.VerifiedPublisher && !rj.Repository.VerifiedPublisher {
return true
}
- // or just more popular
- if ri.Stars > rj.Stars {
- return true
- }
-
// or with more recent app version
-
- if semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion) > 0 {
- return true
+ c := semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion)
+ if c != 0 {
+ return c > 0
}
- return false
+ // shorter repo name is usually closer to officials
+ return len(ri.Repository.Name) < len(rj.Repository.Name)
})
r := results[0]
diff --git a/pkg/dashboard/handlers/kubeHandlers.go b/pkg/dashboard/handlers/kubeHandlers.go
index 60bc28e..7bea038 100644
--- a/pkg/dashboard/handlers/kubeHandlers.go
+++ b/pkg/dashboard/handlers/kubeHandlers.go
@@ -1,15 +1,21 @@
package handlers
import (
- "github.com/joomcode/errorx"
- "k8s.io/apimachinery/pkg/api/errors"
- "net/http"
-
"github.com/gin-gonic/gin"
+ "github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/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"
+ "net/http"
)
+const Unknown = "Unknown"
+const Healthy = "Healthy"
+const Unhealthy = "Unhealthy"
+const Progressing = "Progressing"
+
type KubeHandler struct {
*Contexted
}
@@ -55,20 +61,59 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
c.IndentedJSON(http.StatusOK, res)
}
-func EnhanceStatus(res *v12.Carp) {
- // custom logic to provide most meaningful status for the resource
- if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
- _ = res.Name + ""
- } else if res.Status.Phase == "" && len(res.Status.Conditions) > 0 {
- res.Status.Phase = v12.CarpPhase(res.Status.Conditions[len(res.Status.Conditions)-1].Type)
- res.Status.Message = res.Status.Conditions[len(res.Status.Conditions)-1].Message
- res.Status.Reason = res.Status.Conditions[len(res.Status.Conditions)-1].Reason
- if res.Status.Conditions[len(res.Status.Conditions)-1].Status == "False" {
- res.Status.Phase = "Not" + res.Status.Phase
- }
- } else if res.Status.Phase == "" {
- res.Status.Phase = "Exists"
+func EnhanceStatus(res *v12.Carp) *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 s.Phase == "Error" {
+ c.Status = Unhealthy
+ } else if slices.Contains([]string{"Available", "Active", "Established", "Bound", "Ready"}, string(s.Phase)) {
+ c.Status = Healthy
+ } else if s.Phase == "" && len(s.Conditions) > 0 {
+ for _, cond := range s.Conditions {
+ if cond.Type == "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
+ }
+ } else if cond.Type == "Available" && c.Status == Unknown {
+ if cond.Status == "False" {
+ c.Status = Unhealthy
+ } else {
+ c.Status = Healthy
+ }
+ c.Reason = cond.Reason
+ c.Message = cond.Message
+ }
+ }
+ } else if s.Phase == "Pending" {
+ 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 (h *KubeHandler) Describe(c *gin.Context) {
diff --git a/pkg/dashboard/objects/kubectl.go b/pkg/dashboard/objects/kubectl.go
index 937e385..4eadf31 100644
--- a/pkg/dashboard/objects/kubectl.go
+++ b/pkg/dashboard/objects/kubectl.go
@@ -126,7 +126,14 @@ func (k *K8s) DescribeResource(kind string, ns string, name string) (string, err
func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.Object, error) {
builder := k.Factory.NewBuilder()
- resp := builder.Unstructured().NamespaceParam(namespace).Flatten().ResourceNames(kind, name).Do()
+ builder = builder.Unstructured().SingleResourceType()
+ if namespace != "" {
+ builder = builder.NamespaceParam(namespace)
+ } else {
+ builder = builder.DefaultNamespace()
+ }
+
+ resp := builder.Flatten().ResourceNames(kind, name).Do()
if resp.Err() != nil {
return nil, errorx.Decorate(resp.Err(), "failed to get k8s resource")
}
@@ -139,6 +146,7 @@ func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.
}
func (k *K8s) GetResourceInfo(kind string, namespace string, name string) (*testapiv1.Carp, error) {
+ // TODO: mutex to avoid a lot of requests?
obj, err := k.GetResource(kind, namespace, name)
if err != nil {
return nil, errorx.Decorate(err, "failed to get k8s object")
diff --git a/pkg/dashboard/static/details-view.js b/pkg/dashboard/static/details-view.js
index c8e09c2..944a37a 100644
--- a/pkg/dashboard/static/details-view.js
+++ b/pkg/dashboard/static/details-view.js
@@ -149,7 +149,7 @@ function showResources(namespace, chart, revision) {
const resBody = $("#nav-resources .body");
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
resBody.empty().append('');
- let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources"
+ let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources?health=true"
$.getJSON(url).fail(function (xhr) {
reportError("Failed to get list of resources", xhr)
}).done(function (data) {
@@ -180,22 +180,29 @@ function showResources(namespace, chart, revision) {
resBody.append(resBlock)
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
- $.getJSON("/api/k8s/" + res.kind.toLowerCase() + "/get?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
- //reportError("Failed to get list of resources")
- }).done(function (data) {
- const badge = $("").text(data.status.phase);
- if (["Available", "Active", "Established", "Bound", "Ready"].includes(data.status.phase)) {
+ for (let k = 0; k < res.status.conditions.length; k++) {
+ if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type
+ continue
+ }
+
+ const cond = res.status.conditions[k]
+
+ const badge = $("").text(cond.reason);
+ if (cond.status === "Healthy") {
badge.addClass("bg-success text-dark")
- } else if (["Exists"].includes(data.status.phase)) {
- badge.addClass("bg-success text-dark bg-opacity-50")
- } else if (["Progressing"].includes(data.status.phase)) {
+ } else if (cond.status === "Progressing") {
badge.addClass("bg-warning")
} else {
badge.addClass("bg-danger")
}
+
+ if (["Exists"].includes(cond.reason)) {
+ badge.addClass("bg-opacity-50")
+ }
+
const statusBlock = resBlock.find(".res-status");
- statusBlock.empty().append(badge).attr("title", data.status.phase)
- const statusMessage = getStatusMessage(data.status)
+ statusBlock.empty().append(badge).attr("title", cond.reason)
+ const statusMessage = cond.message
resBlock.find(".res-statusmsg").html("" + (statusMessage ? statusMessage : '') + "")
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
@@ -219,21 +226,11 @@ function showResources(namespace, chart, revision) {
if (badge.hasClass("bg-danger")) {
resBlock.find(".res-statusmsg").append("Troubleshoot in Komodor ")
}
- })
+ }
}
})
}
-function getStatusMessage(status) {
- if (!status) {
- return
- }
- if (status.conditions) {
- return status.conditions[0].message || status.conditions[0].reason
- }
- return status.message || status.reason
-}
-
function showDescribe(ns, kind, name, badge) {
$("#describeModal .offcanvas-header p").text(kind)
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))
diff --git a/pkg/dashboard/static/list-view.js b/pkg/dashboard/static/list-view.js
index b1ebd61..aaae1eb 100644
--- a/pkg/dashboard/static/list-view.js
+++ b/pkg/dashboard/static/list-view.js
@@ -93,7 +93,7 @@ function buildChartCard(elm) {
if (data[0].isSuggestedRepo) {
icon.addClass("bi-plus-circle-fill text-primary")
icon.text(" ADD REPO")
- icon.attr("data-bs-title", "Add '" + data[0].repository+"' to list of known repositories")
+ icon.attr("data-bs-title", "Add '" + data[0].repository + "' to list of known repositories")
} else {
icon.addClass("bi-arrow-up-circle-fill text-primary")
icon.text(" UPGRADE")
@@ -101,12 +101,41 @@ function buildChartCard(elm) {
}
card.find(".rel-chart div").append(icon)
- const tooltipTriggerList = card.find('[data-bs-toggle="tooltip"]')
+ const tooltipTriggerList = card.find('.rel-chart [data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
sendStats('upgradeIconShown', {'isProbable': data[0].isSuggestedRepo})
}
})
+ // check resource health status
+ $.getJSON("/api/helm/releases/" + elm.namespace + "/" + elm.name + "/resources?health=true").fail(function (xhr) {
+ reportError("Failed to find chart in repo", xhr)
+ }).done(function (data) {
+ for (let i = 0; i < data.length; i++) {
+ const res = data[i]
+ for (let k = 0; k < res.status.conditions.length; k++) {
+ if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type
+ continue
+ }
+
+ const cond = res.status.conditions[k]
+ const square=$(" ")
+ if (cond.status === "Healthy") {
+ square.addClass("bg-success")
+ } else if (cond.status === "Progressing") {
+ square.addClass("bg-warning")
+ } else {
+ square.addClass("bg-danger")
+ }
+ square.attr("data-bs-title", cond.status+" "+res.kind+" '"+res.metadata.name+"'")
+ card.find(".rel-status div").append(square)
+ }
+ }
+
+ const tooltipTriggerList = card.find('.rel-status [data-bs-toggle="tooltip"]')
+ const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
+ })
+
return card;
}
diff --git a/pkg/dashboard/static/openapi.json b/pkg/dashboard/static/openapi.json
index 09180f0..67e3be9 100644
--- a/pkg/dashboard/static/openapi.json
+++ b/pkg/dashboard/static/openapi.json
@@ -302,6 +302,11 @@
"name": "name",
"in": "path",
"description": "Name of Helm release"
+ },
+ {
+ "name": "health",
+ "in": "query",
+ "description": "Flag to query k8s health status of resources"
}
],
"get": {
diff --git a/pkg/dashboard/static/styles.css b/pkg/dashboard/static/styles.css
index 58d9eb9..cfed352 100644
--- a/pkg/dashboard/static/styles.css
+++ b/pkg/dashboard/static/styles.css
@@ -300,4 +300,23 @@ nav .nav-tabs .nav-link.active {
.test-result {
font-size: 1rem;
+}
+
+.square {
+ width: 0.55rem;
+ height: 0.55rem;
+ display: inline-block;
+ border-radius: 0.1rem!important;
+}
+
+.square.bg-danger {
+ background-color: #ff0072!important;
+}
+
+.square.bg-warning {
+ background-color: #ffa800!important;
+}
+
+.square.bg-success {
+ background-color: #00c2ab!important;
}
\ No newline at end of file