mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +02:00
feat(helm/templates): helm app templates EE-943 (#5449)
* 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> * feat(helm): list and configure helm chart (#5431) * backport and tidyup code * --amend * using rocket icon for charts * helm chart bugfix - clear category button * added matomo analytics for helm chart install * fix web editor exit warning without changes * editor modified exit bugfix * fixed notifications typo * updated helm template text * helper text to convey slow helm templates load Co-authored-by: zees-dev <dev.786zshan@gmail.com> * removing redundant time-consuming api call by using prop attribute * feat(helm) helm chart backport from ee EE-1311 (#5436) * Add missing defaultHelmRepoUrl and mock testing * Backport EE-1477 * Backport updates to helm tests from EE * add https by default changes and ssl to tls renaming from EE * Port install integration test. Disabled by default to pass CI checks * merged changes from EE for the integration test * kube proxy whitelist updated to support internal helm install command Co-authored-by: zees-dev <dev.786zshan@gmail.com> * Pull in all changes from tech review in EE-943 * added helm to sidebar after rebase, sync CE with EE * bugfix: kubectl shell not opening - bearer token bug * tidy go modules & remove yarn-error.log * removed redundant handler (not used) - to match EE * resolved merge conflicts, updated code * feat(helm/views): helm release and application views EE-1236 (#5529) * 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> * Pull in all changes from tech review in EE-943 * added helm to sidebar after rebase, sync CE with EE * removed redundant handler (not used) - to match EE * feat(helm) display helm charts - backend EE-1236 * copy over components for new applications view EE-1236 * Add new applications datatable component * Add more migrated files * removed test not applicable to CE * baclkported EE app data table code to CE * removed redundant helm repo url * resolved conflicts, updated code * using endpoint middleware * PR review fixes * using constants, openapi updated Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Co-authored-by: zees-dev <dev.786zshan@gmail.com> * fixed test conflicts, go linted * feat(helm/templates-add): helm templates add repo for user support EE-1278 (#5514) * 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> * feat(helm) helm chart backport from ee EE-1311 (#5436) * Add missing defaultHelmRepoUrl and mock testing * Backport EE-1477 * Backport updates to helm tests from EE * add https by default changes and ssl to tls renaming from EE * Port install integration test. Disabled by default to pass CI checks * merged changes from EE for the integration test * kube proxy whitelist updated to support internal helm install command Co-authored-by: zees-dev <dev.786zshan@gmail.com> * Pull in all changes from tech review in EE-943 * 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> * Pull in all changes from tech review in EE-943 * added helm to sidebar after rebase, sync CE with EE * backport EE-1278, squashed, diffed, updated * helm install openapi spec update * resolved conflicts, updated code * - matching ee codebase at 0afe57034449ee0e9f333d92c252a13995a93019 - helm install using endpoint middleware - remove trailing slash from added/persisted helm repo urls * feat(helm) use libhelm url validator and improved path assembly EE-1554 (#5561) * feat(helm/userrepos) fix getting global repo for ordinary users EE-1562 (#5567) * feat(helm/userrepos) fix getting global repo for ordinary users EE-1562 * post review changes and further backported changes from EE * resolved conflicts, updated code * fixed helm_install handler unit test * user cannot add existing repo if suffix is '/' (#5571) * feat(helm/docs) fix broken swagger docs EE-1278 (#5572) * Fix swagger docs * minor correction * fix(helm): migrating code from user handler to helm handler (#5573) * - migrated user_helm_repos to helm endpoint handler - migrated api operations from user factory/service to helm factory/service - passing endpointId into helm service/factory as endpoint provider is deprecated * upgrade libhelm to hide secrets Co-authored-by: Matt Hook <hookenz@gmail.com> * removed duplicate file - due to merge conflict * dependency injection in helm factory Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Co-authored-by: Matt Hook <hookenz@gmail.com> * kubernetes.templates -> kubernetes.templates.helm name conflict fix * Validate the URL added as a public helm repo (#5579) * fix(helm): helm app deletion fix EE-1581 (#5582) * updated helm lib to show correct error on uninstall failure * passing down helm app namespace on deletion * fix(k8s): EE-1591 non-admin users cannot deploy charts containing secrets (#5590) Co-authored-by: Simon Meng <simon.meng@portainer.io> * fix(helm): helm epic bugfixes EE-1582 EE-1593 (#5585) * - trim trailing slash and lowercase before persisting helm repo - browser helm templates url /kubernetes/templates/templates -> /kubernetes/templates/helm - fix publish url - fix helm repo add refresh - semi-fix k8s app expansion * Tidy up swagger documentation related to helm. Make json consistent * fixed helm release page for non-default namespaces * k8s app view table expansion bugfix * EE-1593: publish url load balancer fallback Co-authored-by: Matt Hook <hookenz@gmail.com> * k8s app list fix for charts with deployments containing multiple pods - which use the same label (#5599) * fix(kubernetes): app list view fix for secrets with long keys or values EE-1600 (#5600) * k8s app secrets key value text overflow ellipses * wrapping key value pairs instead of ellipses * fix(helm): helm apps bundling issue across different namespaces EE-1619 (#5602) * helm apps bundling issue across different namespaces * - code comments and indentation to ease reading - moved namespace calc out of loop * feat(helm/test) disable slow helm search test by default EE-1599 (#5598) * skip helm_repo_search as it's an integration test * switch to portainer built in integration test checker * make module order match EE * don't print test struct out when skipping integration test Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Co-authored-by: Matt Hook <hookenz@gmail.com> Co-authored-by: cong meng <mcpacino@gmail.com> Co-authored-by: Simon Meng <simon.meng@portainer.io>
This commit is contained in:
parent
e86a586651
commit
2a60b8fcdf
89 changed files with 3055 additions and 139 deletions
|
@ -16,6 +16,7 @@ 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/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/handler/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
|
@ -47,7 +48,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
|
||||
|
@ -166,6 +169,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,6 +207,8 @@ 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/upload"):
|
||||
|
|
103
api/http/handler/helm/handler.go
Normal file
103
api/http/handler/helm/handler.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/libhelm"
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
handlerActivityContext = "Kubernetes"
|
||||
)
|
||||
|
||||
type requestBouncer interface {
|
||||
AuthenticatedAccess(h http.Handler) http.Handler
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer requestBouncer
|
||||
dataStore portainer.DataStore
|
||||
kubeConfigService kubernetes.KubeConfigService
|
||||
helmPackageManager libhelm.HelmPackageManager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
dataStore: dataStore,
|
||||
helmPackageManager: helmPackageManager,
|
||||
kubeConfigService: kubeConfigService,
|
||||
}
|
||||
|
||||
h.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
|
||||
// `helm list -o json`
|
||||
h.Handle("/{id}/kubernetes/helm",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmList))).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)
|
||||
|
||||
h.Handle("/{id}/kubernetes/helm/repositories",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userGetHelmRepos))).Methods(http.MethodGet)
|
||||
h.Handle("/{id}/kubernetes/helm/repositories",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userCreateHelmRepo))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// NewTemplateHandler creates a template handler to manage endpoint group operations.
|
||||
func NewTemplateHandler(bouncer requestBouncer, helmPackageManager libhelm.HelmPackageManager) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
helmPackageManager: helmPackageManager,
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
|
||||
h.Handle("/templates/helm",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmRepoSearch))).Methods(http.MethodGet)
|
||||
|
||||
// helm show [COMMAND] [CHART] [REPO] flags
|
||||
h.Handle("/templates/helm/{command:chart|values|readme}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmShow))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// getHelmClusterAccess obtains the core k8s cluster access details from request.
|
||||
// The cluster access includes the cluster server url, the user's bearer token and the tls certificate.
|
||||
// The cluster access is passed in as kube config CLI params to helm binary.
|
||||
func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.KubernetesClusterAccess, *httperror.HandlerError) {
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint on request context", err}
|
||||
}
|
||||
|
||||
bearerToken, err := security.ExtractBearerToken(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
|
||||
}
|
||||
|
||||
kubeConfigInternal := handler.kubeConfigService.GetKubeConfigInternal(endpoint.ID, bearerToken)
|
||||
return &options.KubernetesClusterAccess{
|
||||
ClusterServerURL: kubeConfigInternal.ClusterServerURL,
|
||||
CertificateAuthorityFile: kubeConfigInternal.CertificateAuthorityFile,
|
||||
AuthToken: kubeConfigInternal.AuthToken,
|
||||
}, nil
|
||||
}
|
55
api/http/handler/helm/helm_delete.go
Normal file
55
api/http/handler/helm/helm_delete.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id HelmDelete
|
||||
// @summary Delete Helm Release
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param release query string true "The name of the release/application to uninstall"
|
||||
// @param namespace query string true "An optional namespace"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid endpoint id or bad request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Endpoint or ServiceAccount not found"
|
||||
// @failure 500 "Server error or helm error"
|
||||
// @router /endpoints/:id/kubernetes/helm/{release} [delete]
|
||||
func (handler *Handler) helmDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
release, err := request.RetrieveRouteVariableValue(r, "release")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "No release specified", err}
|
||||
}
|
||||
|
||||
clusterAccess, httperr := handler.getHelmClusterAccess(r)
|
||||
if httperr != nil {
|
||||
return httperr
|
||||
}
|
||||
|
||||
uninstallOpts := options.UninstallOptions{
|
||||
Name: release,
|
||||
KubernetesClusterAccess: clusterAccess,
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
if namespace := q.Get("namespace"); namespace != "" {
|
||||
uninstallOpts.Namespace = namespace
|
||||
}
|
||||
|
||||
err = handler.helmPackageManager.Uninstall(uninstallOpts)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Helm returned an error", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
53
api/http/handler/helm/helm_delete_test.go
Normal file
53
api/http/handler/helm/helm_delete_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/portainer/libhelm/options"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
func Test_helmDelete(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1})
|
||||
is.NoError(err, "Error creating endpoint")
|
||||
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "Error creating a user")
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
// Install a single chart directly, to be deleted by the handler
|
||||
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
|
||||
h.helmPackageManager.Install(options)
|
||||
|
||||
t.Run("helmDelete succeeds with admin user", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/1/kubernetes/helm/%s", options.Name), nil)
|
||||
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Add("Authorization", "Bearer dummytoken")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusNoContent, rr.Code, "Status should be 204")
|
||||
})
|
||||
}
|
134
api/http/handler/helm/helm_install.go
Normal file
134
api/http/handler/helm/helm_install.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/kubernetes/validation"
|
||||
)
|
||||
|
||||
type installChartPayload struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
Chart string `json:"chart"`
|
||||
Repo string `json:"repo"`
|
||||
Values string `json:"values"`
|
||||
}
|
||||
|
||||
var errChartNameInvalid = errors.New("invalid chart name. " +
|
||||
"Chart name must consist of lower case alphanumeric characters, '-' or '.'," +
|
||||
" and must start and end with an alphanumeric character",
|
||||
)
|
||||
|
||||
// @id HelmInstall
|
||||
// @summary Install Helm Chart
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param payload body installChartPayload true "Chart details"
|
||||
// @success 201 {object} release.Release "Created"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Endpoint or ServiceAccount not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/:id/kubernetes/helm [post]
|
||||
func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload installChartPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "Invalid Helm install payload",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
release, err := handler.installChart(r, payload)
|
||||
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 (p *installChartPayload) Validate(_ *http.Request) error {
|
||||
var required []string
|
||||
if p.Repo == "" {
|
||||
required = append(required, "repo")
|
||||
}
|
||||
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, ", "))
|
||||
}
|
||||
|
||||
if errs := validation.IsDNS1123Subdomain(p.Name); len(errs) > 0 {
|
||||
return errChartNameInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*release.Release, error) {
|
||||
clusterAccess, httperr := handler.getHelmClusterAccess(r)
|
||||
if httperr != nil {
|
||||
return nil, httperr.Err
|
||||
}
|
||||
installOpts := options.InstallOptions{
|
||||
Name: p.Name,
|
||||
Chart: p.Chart,
|
||||
Namespace: p.Namespace,
|
||||
Repo: p.Repo,
|
||||
KubernetesClusterAccess: &options.KubernetesClusterAccess{
|
||||
ClusterServerURL: clusterAccess.ClusterServerURL,
|
||||
CertificateAuthorityFile: clusterAccess.CertificateAuthorityFile,
|
||||
AuthToken: clusterAccess.AuthToken,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return release, nil
|
||||
}
|
65
api/http/handler/helm/helm_install_test.go
Normal file
65
api/http/handler/helm/helm_install_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_helmInstall(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1})
|
||||
is.NoError(err, "error creating endpoint")
|
||||
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
// Install a single chart. We expect to get these values back
|
||||
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default", Repo: "https://charts.bitnami.com/bitnami"}
|
||||
optdata, err := json.Marshal(options)
|
||||
is.NoError(err)
|
||||
|
||||
t.Run("helmInstall succeeds with admin user", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/1/kubernetes/helm", bytes.NewBuffer(optdata))
|
||||
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Add("Authorization", "Bearer dummytoken")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusCreated, rr.Code, "Status should be 201")
|
||||
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
|
||||
resp := release.Release{}
|
||||
err = json.Unmarshal(body, &resp)
|
||||
is.NoError(err, "response should be json")
|
||||
is.EqualValues(options.Name, resp.Name, "Name doesn't match")
|
||||
is.EqualValues(options.Namespace, resp.Namespace, "Namespace doesn't match")
|
||||
})
|
||||
}
|
63
api/http/handler/helm/helm_list.go
Normal file
63
api/http/handler/helm/helm_list.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id HelmList
|
||||
// @summary List Helm Releases
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param namespace query string true "specify an optional namespace"
|
||||
// @param filter query string true "specify an optional filter"
|
||||
// @param selector query string true "specify an optional selector"
|
||||
// @success 200 {array} release.ReleaseElement "Success"
|
||||
// @failure 400 "Invalid endpoint identifier"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Endpoint or ServiceAccount not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/:id/kubernetes/helm [get]
|
||||
func (handler *Handler) helmList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
clusterAccess, httperr := handler.getHelmClusterAccess(r)
|
||||
if httperr != nil {
|
||||
return httperr
|
||||
}
|
||||
|
||||
listOpts := options.ListOptions{
|
||||
KubernetesClusterAccess: clusterAccess,
|
||||
}
|
||||
|
||||
params := r.URL.Query()
|
||||
|
||||
// optional namespace. The library defaults to "default"
|
||||
namespace, _ := request.RetrieveQueryParameter(r, "namespace", true)
|
||||
if namespace != "" {
|
||||
listOpts.Namespace = namespace
|
||||
}
|
||||
|
||||
// optional filter
|
||||
if filter := params.Get("filter"); filter != "" {
|
||||
listOpts.Filter = filter
|
||||
}
|
||||
|
||||
// optional selector
|
||||
if selector := params.Get("selector"); selector != "" {
|
||||
listOpts.Selector = selector
|
||||
}
|
||||
|
||||
releases, err := handler.helmPackageManager.List(listOpts)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Helm returned an error", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, releases)
|
||||
}
|
60
api/http/handler/helm/helm_list_test.go
Normal file
60
api/http/handler/helm/helm_list_test.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
func Test_helmList(t *testing.T) {
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1})
|
||||
assert.NoError(t, err, "error creating endpoint")
|
||||
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
assert.NoError(t, err, "error creating a user")
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
|
||||
// Install a single chart. We expect to get these values back
|
||||
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
|
||||
h.helmPackageManager.Install(options)
|
||||
|
||||
t.Run("helmList", func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/1/kubernetes/helm", nil)
|
||||
req.Header.Add("Authorization", "Bearer dummytoken")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusOK, rr.Code, "Status should be 200 OK")
|
||||
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
|
||||
data := []release.ReleaseElement{}
|
||||
json.Unmarshal(body, &data)
|
||||
if is.Equal(1, len(data), "Expected one chart entry") {
|
||||
is.EqualValues(options.Name, data[0].Name, "Name doesn't match")
|
||||
is.EqualValues(options.Chart, data[0].Chart, "Chart doesn't match")
|
||||
}
|
||||
})
|
||||
}
|
56
api/http/handler/helm/helm_repo_search.go
Normal file
56
api/http/handler/helm/helm_repo_search.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm"
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
)
|
||||
|
||||
// @id HelmRepoSearch
|
||||
// @summary Search Helm Charts
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {object} string "Success"
|
||||
// @failure 400 "Bad request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /templates/helm [get]
|
||||
func (handler *Handler) helmRepoSearch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
repo := r.URL.Query().Get("repo")
|
||||
if repo == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.New("missing `repo` query parameter")}
|
||||
}
|
||||
|
||||
_, err := url.ParseRequestURI(repo)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo))}
|
||||
}
|
||||
|
||||
searchOpts := options.SearchRepoOptions{
|
||||
Repo: repo,
|
||||
}
|
||||
|
||||
result, err := libhelm.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(result)
|
||||
|
||||
return nil
|
||||
}
|
51
api/http/handler/helm/helm_repo_search_test.go
Normal file
51
api/http/handler/helm/helm_repo_search_test.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
func Test_helmRepoSearch(t *testing.T) {
|
||||
helper.IntegrationTest(t)
|
||||
is := assert.New(t)
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager)
|
||||
|
||||
assert.NotNil(t, h, "Handler should not fail")
|
||||
|
||||
repos := []string{"https://charts.bitnami.com/bitnami", "https://portainer.github.io/k8s"}
|
||||
|
||||
for _, repo := range repos {
|
||||
t.Run(repo, func(t *testing.T) {
|
||||
repoUrlEncoded := url.QueryEscape(repo)
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/templates/helm?repo=%s", repoUrlEncoded), nil)
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusOK, rr.Code, "Status should be 200 OK")
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
is.NotEmpty(body, "Body should not be empty")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("fails on invalid URL", func(t *testing.T) {
|
||||
repo := "abc.com"
|
||||
repoUrlEncoded := url.QueryEscape(repo)
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/templates/helm?repo=%s", repoUrlEncoded), nil)
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusBadRequest, rr.Code, "Status should be 400 Bad request")
|
||||
})
|
||||
}
|
70
api/http/handler/helm/helm_show.go
Normal file
70
api/http/handler/helm/helm_show.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
)
|
||||
|
||||
// @id HelmShow
|
||||
// @summary Show Helm Chart Information
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm_chart
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @param chart query string true "Chart name"
|
||||
// @param command path string false "chart/values/readme"
|
||||
// @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/{command} [get]
|
||||
func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
repo := r.URL.Query().Get("repo")
|
||||
if repo == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.New("missing `repo` query parameter")}
|
||||
}
|
||||
_, err := url.ParseRequestURI(repo)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo))}
|
||||
}
|
||||
|
||||
chart := r.URL.Query().Get("chart")
|
||||
if chart == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.New("missing `chart` query parameter")}
|
||||
}
|
||||
|
||||
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 := options.ShowOptions{
|
||||
OutputFormat: options.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(result)
|
||||
|
||||
return nil
|
||||
}
|
47
api/http/handler/helm/helm_show_test.go
Normal file
47
api/http/handler/helm/helm_show_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_helmShow(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
commands := map[string]string{
|
||||
"values": test.MockDataValues,
|
||||
"chart": test.MockDataChart,
|
||||
"readme": test.MockDataReadme,
|
||||
}
|
||||
|
||||
for cmd, expect := range commands {
|
||||
t.Run(cmd, func(t *testing.T) {
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
repoUrlEncoded := url.QueryEscape("https://charts.bitnami.com/bitnami")
|
||||
chart := "nginx"
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/templates/helm/%s?repo=%s&chart=%s", cmd, repoUrlEncoded, chart), nil)
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(rr.Code, http.StatusOK, "Status should be 200 OK")
|
||||
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
is.EqualValues(string(body), expect, "Unexpected search response")
|
||||
})
|
||||
}
|
||||
}
|
126
api/http/handler/helm/user_helm_repos.go
Normal file
126
api/http/handler/helm/user_helm_repos.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
|
||||
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/http/security"
|
||||
)
|
||||
|
||||
type helmUserRepositoryResponse struct {
|
||||
GlobalRepository string `json:"GlobalRepository"`
|
||||
UserRepositories []portainer.HelmUserRepository `json:"UserRepositories"`
|
||||
}
|
||||
|
||||
type addHelmRepoUrlPayload struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error {
|
||||
return libhelm.ValidateHelmRepositoryURL(p.URL)
|
||||
}
|
||||
|
||||
// @id HelmUserRepositoryCreate
|
||||
// @summary Create a user helm repository
|
||||
// @description Create a user helm repository.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param payload body addHelmRepoUrlPayload true "Helm Repository"
|
||||
// @success 200 {object} portainer.HelmUserRepository "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/:id/kubernetes/helm/repositories [post]
|
||||
func (handler *Handler) userCreateHelmRepo(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
userID := portainer.UserID(tokenData.ID)
|
||||
|
||||
p := new(addHelmRepoUrlPayload)
|
||||
err = request.DecodeAndValidateJSONPayload(r, p)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "Invalid Helm repository URL",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
// lowercase, remove trailing slash
|
||||
p.URL = strings.TrimSuffix(strings.ToLower(p.URL), "/")
|
||||
|
||||
records, err := handler.dataStore.HelmUserRepository().HelmUserRepositoryByUserID(userID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to access the DataStore", err}
|
||||
}
|
||||
|
||||
// check if repo already exists - by doing case insensitive comparison
|
||||
for _, record := range records {
|
||||
if strings.EqualFold(record.URL, p.URL) {
|
||||
errMsg := "Helm repo already registered for user"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
}
|
||||
|
||||
record := portainer.HelmUserRepository{
|
||||
UserID: userID,
|
||||
URL: p.URL,
|
||||
}
|
||||
|
||||
err = handler.dataStore.HelmUserRepository().CreateHelmUserRepository(&record)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to save a user Helm repository URL", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, record)
|
||||
}
|
||||
|
||||
// @id HelmUserRepositoriesList
|
||||
// @summary List a users helm repositories
|
||||
// @description Inspect a user helm repositories.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "User identifier"
|
||||
// @success 200 {object} helmUserRepositoryResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/:id/kubernetes/helm/repositories [get]
|
||||
func (handler *Handler) userGetHelmRepos(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
userID := portainer.UserID(tokenData.ID)
|
||||
|
||||
settings, err := handler.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
userRepos, err := handler.dataStore.HelmUserRepository().HelmUserRepositoryByUserID(userID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get user Helm repositories", err}
|
||||
}
|
||||
|
||||
resp := helmUserRepositoryResponse{
|
||||
GlobalRepository: settings.HelmRepositoryURL,
|
||||
UserRepositories: userRepos,
|
||||
}
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
|
@ -3,6 +3,8 @@ package kubernetes
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
|
@ -10,8 +12,6 @@ import (
|
|||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
kcli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// @id GetKubernetesConfig
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// @id getKubernetesNodesLimits
|
||||
|
@ -18,7 +19,7 @@ import (
|
|||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Endpoint identifier"
|
||||
// @success 200 {object} K8sNodesLimits "Success"
|
||||
// @success 200 {object} portainer.K8sNodesLimits "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 403 "Permission denied"
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
|
@ -36,6 +38,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 +52,12 @@ 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 != "" {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "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 +111,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
settings.TemplatesURL = *payload.TemplatesURL
|
||||
}
|
||||
|
||||
if payload.HelmRepositoryURL != nil {
|
||||
settings.HelmRepositoryURL = strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
}
|
||||
|
||||
if payload.BlackListedLabels != nil {
|
||||
settings.BlackListedLabels = payload.BlackListedLabels
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"errors"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"net/http"
|
||||
|
|
|
@ -158,7 +158,7 @@ func (transport *baseTransport) proxySecretDeleteOperation(request *http.Request
|
|||
}
|
||||
|
||||
func isSecretRepresentPrivateRegistry(secret map[string]interface{}) bool {
|
||||
if secret["type"].(string) != string(v1.SecretTypeDockerConfigJson) {
|
||||
if secret["type"] == nil || secret["type"].(string) != string(v1.SecretTypeDockerConfigJson) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -199,25 +199,14 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h
|
|||
func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var tokenData *portainer.TokenData
|
||||
var token string
|
||||
|
||||
// Optionally, token might be set via the "token" query parameter.
|
||||
// For example, in websocket requests
|
||||
token = r.URL.Query().Get("token")
|
||||
|
||||
// Get token from the Authorization header
|
||||
tokens, ok := r.Header["Authorization"]
|
||||
if ok && len(tokens) >= 1 {
|
||||
token = tokens[0]
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized)
|
||||
// get token from the Authorization header or query parameter
|
||||
token, err := ExtractBearerToken(r)
|
||||
if err != nil {
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", err)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
tokenData, err = bouncer.jwtService.ParseAndVerifyToken(token)
|
||||
if err != nil {
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "Invalid JWT token", err)
|
||||
|
@ -233,12 +222,28 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
|
|||
return
|
||||
}
|
||||
|
||||
ctx := storeTokenData(r, tokenData)
|
||||
ctx := StoreTokenData(r, tokenData)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// ExtractBearerToken extracts the Bearer token from the request header or query parameter and returns the token.
|
||||
func ExtractBearerToken(r *http.Request) (string, error) {
|
||||
// Optionally, token might be set via the "token" query parameter.
|
||||
// For example, in websocket requests
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
tokens, ok := r.Header["Authorization"]
|
||||
if ok && len(tokens) >= 1 {
|
||||
token = tokens[0]
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
if token == "" {
|
||||
return "", httperrors.ErrUnauthorized
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -17,8 +17,8 @@ const (
|
|||
contextRestrictedRequest
|
||||
)
|
||||
|
||||
// storeTokenData stores a TokenData object inside the request context and returns the enhanced context.
|
||||
func storeTokenData(request *http.Request, tokenData *portainer.TokenData) context.Context {
|
||||
// StoreTokenData stores a TokenData object inside the request context and returns the enhanced context.
|
||||
func StoreTokenData(request *http.Request, tokenData *portainer.TokenData) context.Context {
|
||||
return context.WithValue(request.Context(), contextAuthenticationKey, tokenData)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
|
@ -26,6 +27,7 @@ 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"
|
||||
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 +51,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 +79,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 libhelm.HelmPackageManager
|
||||
Scheduler *scheduler.Scheduler
|
||||
ShutdownCtx context.Context
|
||||
ShutdownTrigger context.CancelFunc
|
||||
|
@ -166,6 +171,10 @@ func (server *Server) Start() error {
|
|||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||
|
||||
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.HelmPackageManager, server.KubeConfigService)
|
||||
|
||||
var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager)
|
||||
|
||||
var motdHandler = motd.NewHandler(requestBouncer)
|
||||
|
||||
var registryHandler = registries.NewHandler(requestBouncer)
|
||||
|
@ -242,10 +251,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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue