mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 11:48:04 +00:00
304 lines
7.3 KiB
Go
304 lines
7.3 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"embed"
|
|
"errors"
|
|
"github.com/gin-gonic/gin"
|
|
log "github.com/sirupsen/logrus"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
)
|
|
|
|
//go:embed static/*
|
|
var staticFS embed.FS
|
|
|
|
func noCache(c *gin.Context) {
|
|
c.Header("Cache-Control", "no-cache")
|
|
c.Next()
|
|
}
|
|
|
|
func errorHandler(c *gin.Context) {
|
|
c.Next()
|
|
|
|
errs := ""
|
|
for _, err := range c.Errors {
|
|
log.Debugf("Error: %s", err)
|
|
errs += err.Error() + "\n"
|
|
}
|
|
|
|
if errs != "" {
|
|
c.String(http.StatusInternalServerError, errs)
|
|
}
|
|
}
|
|
|
|
func NewRouter(abortWeb ControlChan, data *DataLayer) *gin.Engine {
|
|
var api *gin.Engine
|
|
if os.Getenv("DEBUG") == "" {
|
|
api = gin.New()
|
|
api.Use(gin.Recovery())
|
|
} else {
|
|
api = gin.Default()
|
|
}
|
|
|
|
api.Use(noCache)
|
|
api.Use(contextSetter(data))
|
|
api.Use(errorHandler)
|
|
configureStatic(api)
|
|
|
|
configureRoutes(abortWeb, data, api)
|
|
return api
|
|
}
|
|
|
|
func configureRoutes(abortWeb ControlChan, data *DataLayer, api *gin.Engine) {
|
|
// server shutdown handler
|
|
api.DELETE("/", func(c *gin.Context) {
|
|
abortWeb <- struct{}{}
|
|
})
|
|
|
|
configureHelms(api, data)
|
|
configureKubectls(api, data)
|
|
}
|
|
|
|
func configureHelms(api *gin.Engine, data *DataLayer) {
|
|
api.GET("/api/helm/charts", func(c *gin.Context) {
|
|
res, err := data.ListInstalled()
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
c.IndentedJSON(http.StatusOK, res)
|
|
})
|
|
|
|
api.DELETE("/api/helm/charts", func(c *gin.Context) {
|
|
qp, err := getQueryProps(c, false)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
|
}
|
|
err = data.UninstallChart(qp.Namespace, qp.Name)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
c.Redirect(http.StatusFound, "/")
|
|
})
|
|
|
|
api.POST("/api/helm/charts/rollback", func(c *gin.Context) {
|
|
qp, err := getQueryProps(c, true)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
err = data.Revert(qp.Namespace, qp.Name, qp.Revision)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
c.Redirect(http.StatusFound, "/")
|
|
})
|
|
|
|
api.GET("/api/helm/charts/history", func(c *gin.Context) {
|
|
qp, err := getQueryProps(c, false)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
res, err := data.ChartHistory(qp.Namespace, qp.Name)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
c.IndentedJSON(http.StatusOK, res)
|
|
})
|
|
|
|
api.GET("/api/helm/charts/resources", func(c *gin.Context) {
|
|
qp, err := getQueryProps(c, true)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
res, err := data.RevisionManifestsParsed(qp.Namespace, qp.Name, qp.Revision)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
c.IndentedJSON(http.StatusOK, res)
|
|
})
|
|
|
|
api.GET("/api/helm/charts/:section", func(c *gin.Context) {
|
|
qp, err := getQueryProps(c, true)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
flag := c.Query("flag") == "true"
|
|
rDiff := c.Query("revisionDiff")
|
|
res, err := handleGetSection(data, c.Param("section"), rDiff, qp, flag)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
}
|
|
c.String(http.StatusOK, res)
|
|
})
|
|
}
|
|
|
|
func handleGetSection(data *DataLayer, section string, rDiff string, qp *QueryProps, flag bool) (string, error) {
|
|
sections := map[string]SectionFn{
|
|
"manifests": data.RevisionManifests,
|
|
"values": data.RevisionValues,
|
|
"notes": data.RevisionNotes,
|
|
}
|
|
|
|
functor, found := sections[section]
|
|
if !found {
|
|
return "", errors.New("unsupported section: " + section)
|
|
}
|
|
|
|
if rDiff != "" {
|
|
cRevDiff, err := strconv.Atoi(rDiff)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ext := ".yaml"
|
|
if section == "notes" {
|
|
ext = ".txt"
|
|
}
|
|
|
|
res, err := RevisionDiff(functor, ext, qp.Namespace, qp.Name, cRevDiff, qp.Revision, flag)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return res, nil
|
|
} else {
|
|
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return res, nil
|
|
}
|
|
}
|
|
|
|
func configureKubectls(api *gin.Engine, data *DataLayer) {
|
|
api.GET("/api/kube/contexts", func(c *gin.Context) {
|
|
res, err := data.ListContexts()
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
c.IndentedJSON(http.StatusOK, res)
|
|
})
|
|
|
|
api.GET("/api/kube/resources/:kind", func(c *gin.Context) {
|
|
qp, err := getQueryProps(c, false)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
res, err := data.GetResource(qp.Namespace, &GenericResource{
|
|
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
|
|
ObjectMeta: v1.ObjectMeta{Name: qp.Name},
|
|
})
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
c.IndentedJSON(http.StatusOK, res)
|
|
})
|
|
|
|
api.GET("/api/kube/describe/:kind", func(c *gin.Context) {
|
|
qp, err := getQueryProps(c, false)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
res, err := data.DescribeResource(qp.Namespace, c.Param("kind"), qp.Name)
|
|
if err != nil {
|
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
c.String(http.StatusOK, res)
|
|
})
|
|
}
|
|
|
|
func configureStatic(api *gin.Engine) {
|
|
fs := http.FS(staticFS)
|
|
|
|
// local dev speed-up
|
|
localDevPath := "pkg/dashboard/static"
|
|
if _, err := os.Stat(localDevPath); err == nil {
|
|
log.Warnf("Using local development path to serve static files")
|
|
|
|
// the root page
|
|
api.GET("/", func(c *gin.Context) {
|
|
c.File(path.Join(localDevPath, "index.html"))
|
|
})
|
|
|
|
// serve a directory called static
|
|
api.GET("/static/*filepath", func(c *gin.Context) {
|
|
c.File(path.Join(localDevPath, c.Param("filepath")))
|
|
})
|
|
} else {
|
|
// the root page
|
|
api.GET("/", func(c *gin.Context) {
|
|
c.FileFromFS("/static/", fs)
|
|
})
|
|
|
|
// serve a directory called static
|
|
api.GET("/static/*filepath", func(c *gin.Context) {
|
|
c.FileFromFS(c.Request.URL.Path, fs)
|
|
})
|
|
}
|
|
}
|
|
|
|
func contextSetter(data *DataLayer) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if context, ok := c.Request.Header["X-Kubecontext"]; ok {
|
|
log.Debugf("Setting current context to: %s", context)
|
|
data.KubeContext = context[0]
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
type QueryProps struct {
|
|
Namespace string
|
|
Name string
|
|
Revision int
|
|
}
|
|
|
|
func getQueryProps(c *gin.Context, revRequired bool) (*QueryProps, error) {
|
|
qp := QueryProps{}
|
|
|
|
qp.Namespace = c.Query("namespace")
|
|
qp.Name = c.Query("name")
|
|
if qp.Name == "" {
|
|
return nil, errors.New("missing required query string parameter: name")
|
|
}
|
|
|
|
cRev, err := strconv.Atoi(c.Query("revision"))
|
|
if err != nil && revRequired {
|
|
return nil, err
|
|
}
|
|
qp.Revision = cRev
|
|
|
|
return &qp, nil
|
|
}
|