feat(helm): add helm chart backport to ce EE-1409 (#5425)

* EE-1311 Helm Chart Backport from EE

* backport to ce

Co-authored-by: Matt Hook <hookenz@gmail.com>
This commit is contained in:
Richard Wei
2021-08-15 19:50:54 +12:00
committed by zees-dev
parent a176ec5ace
commit 0cf5f11d28
40 changed files with 1976 additions and 1 deletions

View File

@@ -16,6 +16,8 @@ import (
"github.com/portainer/portainer/api/http/handler/endpointproxy"
"github.com/portainer/portainer/api/http/handler/endpoints"
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/helm"
"github.com/portainer/portainer/api/http/handler/helmcharts"
"github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
@@ -47,7 +49,9 @@ type Handler struct {
EndpointEdgeHandler *endpointedge.Handler
EndpointGroupHandler *endpointgroups.Handler
EndpointHandler *endpoints.Handler
EndpointHelmHandler *helm.Handler
EndpointProxyHandler *endpointproxy.Handler
HelmTemplatesHandler *helm.Handler
KubernetesHandler *kubernetes.Handler
FileHandler *file.Handler
MOTDHandler *motd.Handler
@@ -62,6 +66,7 @@ type Handler struct {
TeamMembershipHandler *teammemberships.Handler
TeamHandler *teams.Handler
TemplatesHandler *templates.Handler
HelmchartsHandler *helmcharts.Handler
UploadHandler *upload.Handler
UserHandler *users.Handler
WebSocketHandler *websocket.Handler
@@ -166,6 +171,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r)
// Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm
case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"):
http.StripPrefix("/api/endpoints", h.EndpointHelmHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
switch {
case strings.Contains(r.URL.Path, "/docker/"):
@@ -199,8 +209,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/tags"):
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/templates/helm"):
http.StripPrefix("/api", h.HelmTemplatesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/templates"):
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/helmcharts"):
http.StripPrefix("/api", h.HelmchartsHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/upload"):
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/users"):

View File

@@ -0,0 +1,127 @@
package helm
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/exec/helm"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
)
const (
handlerActivityContext = "Kubernetes"
)
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
DataStore portainer.DataStore
HelmPackageManager helm.HelmPackageManager
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
// `helm list -o json`
h.Handle("/{id}/kubernetes/helm",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmList))).Methods(http.MethodGet)
// `helm get manifest RELEASE_NAME`
h.Handle("/{id}/kubernetes/helm/{release}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmGet))).Methods(http.MethodGet)
// `helm delete RELEASE_NAME`
h.Handle("/{id}/kubernetes/helm/{release}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmDelete))).Methods(http.MethodDelete)
// `helm install [NAME] [CHART] flags`
h.Handle("/{id}/kubernetes/helm",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmInstall))).Methods(http.MethodPost)
return h
}
// NewTemplateHandler creates a template handler to manage endpoint group operations.
func NewTemplateHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
// `helm search [COMMAND] [CHART] flags`
h.Handle("/templates/helm",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmRepoSearch))).Methods(http.MethodGet)
// `helm show [COMMAND] [CHART] flags`
h.Handle("/templates/helm/{chart}/{command:chart|values|readme}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmShow))).Methods(http.MethodGet)
return h
}
// GetEndpoint returns the portainer.Endpoint for the request
func (handler *Handler) GetEndpoint(r *http.Request) (*portainer.Endpoint, *httperror.HandlerError) {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return nil, &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
return endpoint, nil
}
// getHelmRepositoryUrl gets the helm repository url from settings
func (handler *Handler) getHelmRepositoryUrl() (string, *httperror.HandlerError) {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return "", &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to retrieve settings",
Err: err,
}
}
repo := settings.HelmRepositoryURL
if repo == "" {
repo = defaultHelmRepoURL
}
return repo, nil
}
// getProxyUrl generates portainer proxy url which acts as proxy to k8s api server
func getProxyUrl(r *http.Request, endpointID portainer.EndpointID) string {
return fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpointID)
}
// extractBearerToken extracts user's portainer bearer token from request auth header
func extractBearerToken(r *http.Request) (string, error) {
token := ""
tokens := r.Header["Authorization"]
if len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
return "", httperrors.ErrUnauthorized
}
return token, nil
}

View File

@@ -0,0 +1,26 @@
package helm
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
)
// @id HelmDelete
// @summary Delete Helm Chart(s)
// @description
// @description **Access policy**: authorized
// @tags helm_chart
// @security jwt
// @accept json
// @produce json
// @param
// @success 204 {object} portainer.Helm "Success" - TODO
// @failure 401 "Unauthorized"
// @failure 404 "Endpoint or ServiceAccount not found"
// @failure 500 "Server error"
// @router /kubernetes/helm/{release} [delete]
func (handler *Handler) helmDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
w.Write([]byte("Helm Delete"))
return nil
}

View File

@@ -0,0 +1,40 @@
package helm
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
)
// @id HelmGet
// @summary Get Helm Chart(s)
// @description
// @description **Access policy**: authorized
// @tags helm_chart
// @security jwt
// @accept json
// @produce json
// @param
// @success 200 {object} portainer.Helm "Success" - TODO
// @failure 401 "Unauthorized"
// @failure 404 "Endpoint or ServiceAccount not found"
// @failure 500 "Server error"
// @router /kubernetes/helm/{release} [get]
func (handler *Handler) helmGet(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
_, httperr := handler.GetEndpoint(r)
if httperr != nil {
return httperr
}
// TODO
args := []string{}
result, err := handler.HelmPackageManager.Run("get", args, "", "")
if err != nil {
return nil
}
w.Write([]byte(result))
return nil
}

View File

@@ -0,0 +1,136 @@
package helm
import (
"fmt"
"net/http"
"os"
"strings"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/exec/helm"
"github.com/portainer/portainer/api/exec/helm/release"
)
const defaultHelmRepoURL = "https://charts.bitnami.com/bitnami"
type installChartPayload struct {
Namespace string `json:namespace`
Name string `json:"name"`
Chart string `json:"chart"`
Values string `json:"values"`
}
func (p *installChartPayload) Validate(_ *http.Request) error {
var required []string
if p.Name == "" {
required = append(required, "name")
}
if p.Namespace == "" {
required = append(required, "namespace")
}
if p.Chart == "" {
required = append(required, "chart")
}
if len(required) > 0 {
return fmt.Errorf("required field(s) missing: %s", strings.Join(required, ", "))
}
return nil
}
func readPayload(r *http.Request) (*installChartPayload, error) {
p := new(installChartPayload)
err := request.DecodeAndValidateJSONPayload(r, p)
if err != nil {
return nil, err
}
return p, nil
}
// @id HelmInstall
// @summary Install Helm Chart
// @description
// @description **Access policy**: authorized
// @tags helm_chart
// @security jwt
// @accept json
// @produce json
// @param body installChartPayload true "EdgeGroup data when method is string"
// @success 201 {object} helm.Release "Created"
// @failure 401 "Unauthorized"
// @failure 404 "Endpoint or ServiceAccount not found"
// @failure 500 "Server error"
// @router /kubernetes/helm/{release} [post]
func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, httperr := handler.GetEndpoint(r)
if httperr != nil {
return httperr
}
bearerToken, err := extractBearerToken(r)
if err != nil {
return &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
}
repo, httperr := handler.getHelmRepositoryUrl()
if httperr != nil {
return httperr
}
payload, err := readPayload(r)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusBadRequest,
Message: "Invalid Helm install payload",
Err: err,
}
}
release, err := handler.installChart(repo, endpoint, payload, getProxyUrl(r, endpoint.ID), bearerToken)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to install a chart",
Err: err,
}
}
w.WriteHeader(http.StatusCreated)
return response.JSON(w, release)
}
func (handler *Handler) installChart(repo string, endpoint *portainer.Endpoint, p *installChartPayload, serverURL, bearerToken string) (*release.Release, error) {
installOpts := helm.InstallOptions{
Name: p.Name,
Chart: p.Chart,
Namespace: p.Namespace,
Repo: repo,
}
if p.Values != "" {
file, err := os.CreateTemp("", "helm-values")
if err != nil {
return nil, err
}
defer os.Remove(file.Name())
_, err = file.WriteString(p.Values)
if err != nil {
file.Close()
return nil, err
}
err = file.Close()
if err != nil {
return nil, err
}
installOpts.ValuesFile = file.Name()
}
release, err := handler.HelmPackageManager.Install(installOpts, serverURL, bearerToken)
if err != nil {
return nil, err
}
return release, nil
}

View File

@@ -0,0 +1,60 @@
package helm
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
)
// @id HelmList
// @summary List Helm Chart(s)
// @description
// @description **Access policy**: authorized
// @tags helm_chart
// @security jwt
// @accept json
// @produce json
// @param
// @success 200 {object} portainer.Helm "Success" - TODO
// @failure 401 "Unauthorized"
// @failure 404 "Endpoint or ServiceAccount not found"
// @failure 500 "Server error"
// @router /kubernetes/helm/list [get]
func (handler *Handler) helmList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
// TODO read query params
// - namespace (default all-namespaces, no-query-param)
// - filter (release-name)
// - output (default JSON)
endpoint, httperr := handler.GetEndpoint(r)
if httperr != nil {
return httperr
}
proxyServerURL := getProxyUrl(r, endpoint.ID)
bearerToken, err := extractBearerToken(r)
if err != nil {
return &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
}
v := r.URL.Query()
namespace := v.Get("namespace") // Optional
args := []string{"-o", "json"}
if namespace != "" {
args = append(args, "--namespace", namespace)
}
result, err := handler.HelmPackageManager.Run("list", args, proxyServerURL, bearerToken)
if err != nil {
return nil
}
// TODO - return struct - document type in swagger
w.Write([]byte(result))
return nil
}

View File

@@ -0,0 +1,46 @@
package helm
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api/exec/helm"
)
// @id HelmRepoSearch
// @summary Search Helm Charts
// @description
// @description **Access policy**: authorized
// @tags helm_chart
// @security jwt
// @accept json
// @produce json
// @success 200 {object} string "Success"
// @failure 401 "Unauthorized"
// @failure 404 "Endpoint or ServiceAccount not found"
// @failure 500 "Server error"
// @router /templates/helm [get]
func (handler *Handler) helmRepoSearch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
repo, httperr := handler.getHelmRepositoryUrl()
if httperr != nil {
return httperr
}
searchOpts := helm.SearchRepoOptions{
Repo: repo,
}
result, err := handler.HelmPackageManager.SearchRepo(searchOpts)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Search failed",
Err: err,
}
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(result))
return nil
}

View File

@@ -0,0 +1,64 @@
package helm
import (
"log"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api/exec/helm"
)
// @id HelmList
// @summary List Helm Chart(s)
// @description
// @description **Access policy**: authorized
// @tags helm_chart
// @security jwt
// @accept json
// @produce text/plain
// @success 200 {object} string "Success"
// @failure 401 "Unauthorized"
// @failure 404 "Endpoint or ServiceAccount not found"
// @failure 500 "Server error"
// @router /templates/helm/{chart}/{command} [get]
func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
repo, httperr := handler.getHelmRepositoryUrl()
if httperr != nil {
return httperr
}
chart, err := request.RetrieveRouteVariableValue(r, "chart")
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Missing chart name for show",
Err: err,
}
}
cmd, err := request.RetrieveRouteVariableValue(r, "command")
if err != nil {
cmd = "all"
log.Printf("[DEBUG] [internal,helm] [message: command not provided, defaulting to %s]", cmd)
}
showOptions := helm.ShowOptions{
OutputFormat: helm.ShowOutputFormat(cmd),
Chart: chart,
Repo: repo,
}
result, err := handler.HelmPackageManager.Show(showOptions)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to show chart",
Err: err,
}
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(result))
return nil
}

View File

@@ -0,0 +1,29 @@
package helmcharts
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler represents an HTTP API handler for managing templates.
type Handler struct {
*mux.Router
DataStore portainer.DataStore
GitService portainer.GitService
FileService portainer.FileService
}
// NewHandler returns a new instance of Handler.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/helmcharts",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
return h
}

View File

@@ -0,0 +1,46 @@
package helmcharts
import (
"io"
"net/http"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
)
// introduced for swagger
type listResponse struct {
Version string
Templates []portainer.Template
}
// @id TemplateList
// @summary List available templates
// @description List available templates.
// @description **Access policy**: restricted
// @tags templates
// @security jwt
// @produce json
// @success 200 {object} listResponse "Success"
// @failure 500 "Server error"
// @router /templates [get]
func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
resp, err := http.Get(settings.HelmRepositoryURL)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve templates via the network", err}
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json")
_, err = io.Copy(w, resp.Body)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to write templates from templates URL", err}
}
return nil
}

View File

@@ -36,6 +36,8 @@ type settingsUpdatePayload struct {
KubeconfigExpiry *string `example:"24h" default:"0"`
// Whether telemetry is enabled
EnableTelemetry *bool `example:"false"`
// Helm repository URL
HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"`
}
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@@ -48,6 +50,9 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) {
return errors.New("Invalid external templates URL. Must correspond to a valid URL format")
}
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" && !govalidator.IsURL(*payload.HelmRepositoryURL) {
return errors.New("Invalid Helm repository URL. Must correspond to a valid URL format")
}
if payload.UserSessionTimeout != nil {
_, err := time.ParseDuration(*payload.UserSessionTimeout)
if err != nil {
@@ -101,6 +106,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.TemplatesURL = *payload.TemplatesURL
}
if payload.HelmRepositoryURL != nil {
settings.HelmRepositoryURL = *payload.HelmRepositoryURL
}
if payload.BlackListedLabels != nil {
settings.BlackListedLabels = payload.BlackListedLabels
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/exec/helm"
"github.com/portainer/portainer/api/http/handler"
"github.com/portainer/portainer/api/http/handler/auth"
"github.com/portainer/portainer/api/http/handler/backup"
@@ -26,6 +27,8 @@ import (
"github.com/portainer/portainer/api/http/handler/endpointproxy"
"github.com/portainer/portainer/api/http/handler/endpoints"
"github.com/portainer/portainer/api/http/handler/file"
helmhandler "github.com/portainer/portainer/api/http/handler/helm"
"github.com/portainer/portainer/api/http/handler/helmcharts"
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
@@ -49,6 +52,7 @@ import (
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/ssl"
k8s "github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/scheduler"
stackdeployer "github.com/portainer/portainer/api/stacks"
@@ -76,11 +80,13 @@ type Server struct {
SwarmStackManager portainer.SwarmStackManager
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
KubeConfigService k8s.KubeConfigService
Handler *handler.Handler
SSLService *ssl.Service
DockerClientFactory *docker.ClientFactory
KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer
HelmPackageManager helm.HelmPackageManager
Scheduler *scheduler.Scheduler
ShutdownCtx context.Context
ShutdownTrigger context.CancelFunc
@@ -165,6 +171,14 @@ func (server *Server) Start() error {
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
var endpointHelmHandler = helmhandler.NewHandler(requestBouncer)
endpointHelmHandler.DataStore = server.DataStore
endpointHelmHandler.HelmPackageManager = server.HelmPackageManager
var helmTemplatesHandler = helmhandler.NewTemplateHandler(requestBouncer)
helmTemplatesHandler.DataStore = server.DataStore
helmTemplatesHandler.HelmPackageManager = server.HelmPackageManager
var motdHandler = motd.NewHandler(requestBouncer)
var registryHandler = registries.NewHandler(requestBouncer)
@@ -213,6 +227,9 @@ func (server *Server) Start() error {
templatesHandler.FileService = server.FileService
templatesHandler.GitService = server.GitService
var helmchartsHandler = helmcharts.NewHandler(requestBouncer)
helmchartsHandler.DataStore = server.DataStore
var uploadHandler = upload.NewHandler(requestBouncer)
uploadHandler.FileService = server.FileService
@@ -241,10 +258,12 @@ func (server *Server) Start() error {
EdgeTemplatesHandler: edgeTemplatesHandler,
EndpointGroupHandler: endpointGroupHandler,
EndpointHandler: endpointHandler,
EndpointHelmHandler: endpointHelmHandler,
EndpointEdgeHandler: endpointEdgeHandler,
EndpointProxyHandler: endpointProxyHandler,
KubernetesHandler: kubernetesHandler,
FileHandler: fileHandler,
HelmTemplatesHandler: helmTemplatesHandler,
KubernetesHandler: kubernetesHandler,
MOTDHandler: motdHandler,
RegistryHandler: registryHandler,
ResourceControlHandler: resourceControlHandler,
@@ -256,6 +275,7 @@ func (server *Server) Start() error {
TeamHandler: teamHandler,
TeamMembershipHandler: teamMembershipHandler,
TemplatesHandler: templatesHandler,
HelmchartsHandler: helmchartsHandler,
UploadHandler: uploadHandler,
UserHandler: userHandler,
WebSocketHandler: websocketHandler,