mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 11:48:04 +00:00
Display resource health aggregate icons on list of releases (#235)
* Display aggregate resource health status * Reuse old API request, show icons * Take progress indication from deployment conditions * Improve status * Cleanup * Fixups * Squares approach
This commit is contained in:
@@ -136,6 +136,26 @@ func (h *HelmHandler) Resources(c *gin.Context) {
|
|||||||
return
|
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)
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,23 +607,24 @@ func (h *HelmHandler) repoFromArtifactHub(name string) (string, error) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// more popular
|
||||||
|
if ri.Stars != rj.Stars {
|
||||||
|
return ri.Stars > rj.Stars
|
||||||
|
}
|
||||||
|
|
||||||
// or from verified publishers
|
// or from verified publishers
|
||||||
if ri.Repository.VerifiedPublisher && !rj.Repository.VerifiedPublisher {
|
if ri.Repository.VerifiedPublisher && !rj.Repository.VerifiedPublisher {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// or just more popular
|
|
||||||
if ri.Stars > rj.Stars {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// or with more recent app version
|
// or with more recent app version
|
||||||
|
c := semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion)
|
||||||
if semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion) > 0 {
|
if c != 0 {
|
||||||
return true
|
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]
|
r := results[0]
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/joomcode/errorx"
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/joomcode/errorx"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
"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"
|
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 {
|
type KubeHandler struct {
|
||||||
*Contexted
|
*Contexted
|
||||||
}
|
}
|
||||||
@@ -55,20 +61,59 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
|
|||||||
c.IndentedJSON(http.StatusOK, res)
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnhanceStatus(res *v12.Carp) {
|
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
|
// custom logic to provide most meaningful status for the resource
|
||||||
if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
|
if s.Phase == "Error" {
|
||||||
_ = res.Name + ""
|
c.Status = Unhealthy
|
||||||
} else if res.Status.Phase == "" && len(res.Status.Conditions) > 0 {
|
} else if slices.Contains([]string{"Available", "Active", "Established", "Bound", "Ready"}, string(s.Phase)) {
|
||||||
res.Status.Phase = v12.CarpPhase(res.Status.Conditions[len(res.Status.Conditions)-1].Type)
|
c.Status = Healthy
|
||||||
res.Status.Message = res.Status.Conditions[len(res.Status.Conditions)-1].Message
|
} else if s.Phase == "" && len(s.Conditions) > 0 {
|
||||||
res.Status.Reason = res.Status.Conditions[len(res.Status.Conditions)-1].Reason
|
for _, cond := range s.Conditions {
|
||||||
if res.Status.Conditions[len(res.Status.Conditions)-1].Status == "False" {
|
if cond.Type == "Progressing" { // https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
|
||||||
res.Status.Phase = "Not" + res.Status.Phase
|
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 res.Status.Phase == "" {
|
} else if cond.Type == "Available" && c.Status == Unknown {
|
||||||
res.Status.Phase = "Exists"
|
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) {
|
func (h *KubeHandler) Describe(c *gin.Context) {
|
||||||
|
|||||||
@@ -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) {
|
func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.Object, error) {
|
||||||
builder := k.Factory.NewBuilder()
|
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 {
|
if resp.Err() != nil {
|
||||||
return nil, errorx.Decorate(resp.Err(), "failed to get k8s resource")
|
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) {
|
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)
|
obj, err := k.GetResource(kind, namespace, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "failed to get k8s object")
|
return nil, errorx.Decorate(err, "failed to get k8s object")
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ function showResources(namespace, chart, revision) {
|
|||||||
const resBody = $("#nav-resources .body");
|
const resBody = $("#nav-resources .body");
|
||||||
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
|
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
|
||||||
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
|
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
|
||||||
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources"
|
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources?health=true"
|
||||||
$.getJSON(url).fail(function (xhr) {
|
$.getJSON(url).fail(function (xhr) {
|
||||||
reportError("Failed to get list of resources", xhr)
|
reportError("Failed to get list of resources", xhr)
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
@@ -180,22 +180,29 @@ function showResources(namespace, chart, revision) {
|
|||||||
|
|
||||||
resBody.append(resBlock)
|
resBody.append(resBlock)
|
||||||
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
||||||
$.getJSON("/api/k8s/" + res.kind.toLowerCase() + "/get?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
|
for (let k = 0; k < res.status.conditions.length; k++) {
|
||||||
//reportError("Failed to get list of resources")
|
if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type
|
||||||
}).done(function (data) {
|
continue
|
||||||
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
|
}
|
||||||
if (["Available", "Active", "Established", "Bound", "Ready"].includes(data.status.phase)) {
|
|
||||||
|
const cond = res.status.conditions[k]
|
||||||
|
|
||||||
|
const badge = $("<span class='badge me-2 fw-normal'></span>").text(cond.reason);
|
||||||
|
if (cond.status === "Healthy") {
|
||||||
badge.addClass("bg-success text-dark")
|
badge.addClass("bg-success text-dark")
|
||||||
} else if (["Exists"].includes(data.status.phase)) {
|
} else if (cond.status === "Progressing") {
|
||||||
badge.addClass("bg-success text-dark bg-opacity-50")
|
|
||||||
} else if (["Progressing"].includes(data.status.phase)) {
|
|
||||||
badge.addClass("bg-warning")
|
badge.addClass("bg-warning")
|
||||||
} else {
|
} else {
|
||||||
badge.addClass("bg-danger")
|
badge.addClass("bg-danger")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (["Exists"].includes(cond.reason)) {
|
||||||
|
badge.addClass("bg-opacity-50")
|
||||||
|
}
|
||||||
|
|
||||||
const statusBlock = resBlock.find(".res-status");
|
const statusBlock = resBlock.find(".res-status");
|
||||||
statusBlock.empty().append(badge).attr("title", data.status.phase)
|
statusBlock.empty().append(badge).attr("title", cond.reason)
|
||||||
const statusMessage = getStatusMessage(data.status)
|
const statusMessage = cond.message
|
||||||
resBlock.find(".res-statusmsg").html("<span class='text-muted small me-2'>" + (statusMessage ? statusMessage : '') + "</span>")
|
resBlock.find(".res-statusmsg").html("<span class='text-muted small me-2'>" + (statusMessage ? statusMessage : '') + "</span>")
|
||||||
|
|
||||||
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
|
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
|
||||||
@@ -219,21 +226,11 @@ function showResources(namespace, chart, revision) {
|
|||||||
if (badge.hasClass("bg-danger")) {
|
if (badge.hasClass("bg-danger")) {
|
||||||
resBlock.find(".res-statusmsg").append("<a href='" + KomodorCTALink + "' class='btn btn-primary btn-sm fw-normal fs-80' target='_blank'>Troubleshoot in Komodor <i class='bi-box-arrow-up-right'></i></a>")
|
resBlock.find(".res-statusmsg").append("<a href='" + KomodorCTALink + "' class='btn btn-primary btn-sm fw-normal fs-80' target='_blank'>Troubleshoot in Komodor <i class='bi-box-arrow-up-right'></i></a>")
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function showDescribe(ns, kind, name, badge) {
|
||||||
$("#describeModal .offcanvas-header p").text(kind)
|
$("#describeModal .offcanvas-header p").text(kind)
|
||||||
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))
|
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))
|
||||||
|
|||||||
@@ -101,12 +101,41 @@ function buildChartCard(elm) {
|
|||||||
}
|
}
|
||||||
card.find(".rel-chart div").append(icon)
|
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))
|
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||||
sendStats('upgradeIconShown', {'isProbable': data[0].isSuggestedRepo})
|
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=$("<span class='me-1 mb-1 square rounded rounded-1' data-bs-toggle='tooltip'> </span>")
|
||||||
|
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;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,11 @@
|
|||||||
"name": "name",
|
"name": "name",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"description": "Name of Helm release"
|
"description": "Name of Helm release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "health",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Flag to query k8s health status of resources"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"get": {
|
"get": {
|
||||||
|
|||||||
@@ -301,3 +301,22 @@ nav .nav-tabs .nav-link.active {
|
|||||||
.test-result {
|
.test-result {
|
||||||
font-size: 1rem;
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user