diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go
index 6972c34df..d3454bc02 100644
--- a/api/http/handler/handler.go
+++ b/api/http/handler/handler.go
@@ -81,6 +81,7 @@ type Handler struct {
UserHandler *users.Handler
WebSocketHandler *websocket.Handler
WebhookHandler *webhooks.Handler
+ UserHelmHandler *helm.Handler
}
// @title PortainerCE API
diff --git a/api/http/handler/helm/handler.go b/api/http/handler/helm/handler.go
index e95263e57..86352c375 100644
--- a/api/http/handler/helm/handler.go
+++ b/api/http/handler/helm/handler.go
@@ -52,10 +52,11 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
h.Handle("/{id}/kubernetes/helm",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmInstall))).Methods(http.MethodPost)
+ // Deprecated
h.Handle("/{id}/kubernetes/helm/repositories",
- bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userGetHelmRepos))).Methods(http.MethodGet)
+ httperror.LoggerHandler(h.userGetHelmRepos)).Methods(http.MethodGet)
h.Handle("/{id}/kubernetes/helm/repositories",
- bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userCreateHelmRepo))).Methods(http.MethodPost)
+ httperror.LoggerHandler(h.userCreateHelmRepo)).Methods(http.MethodPost)
return h
}
diff --git a/api/http/handler/helm/user_helm_repos.go b/api/http/handler/helm/user_helm_repos.go
index 1c159955a..886d44530 100644
--- a/api/http/handler/helm/user_helm_repos.go
+++ b/api/http/handler/helm/user_helm_repos.go
@@ -27,7 +27,7 @@ func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error {
return libhelm.ValidateHelmRepositoryURL(p.URL, nil)
}
-// @id HelmUserRepositoryCreate
+// @id HelmUserRepositoryCreateDeprecated
// @summary Create a user helm repository
// @description Create a user helm repository.
// @description **Access policy**: authenticated
@@ -42,6 +42,7 @@ func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error {
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 500 "Server error"
+// @deprecated
// @router /endpoints/{id}/kubernetes/helm/repositories [post]
func (handler *Handler) userCreateHelmRepo(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
tokenData, err := security.RetrieveTokenData(r)
@@ -85,7 +86,7 @@ func (handler *Handler) userCreateHelmRepo(w http.ResponseWriter, r *http.Reques
return response.JSON(w, record)
}
-// @id HelmUserRepositoriesList
+// @id HelmUserRepositoriesListDeprecated
// @summary List a users helm repositories
// @description Inspect a user helm repositories.
// @description **Access policy**: authenticated
@@ -98,6 +99,7 @@ func (handler *Handler) userCreateHelmRepo(w http.ResponseWriter, r *http.Reques
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 500 "Server error"
+// @deprecated
// @router /endpoints/{id}/kubernetes/helm/repositories [get]
func (handler *Handler) userGetHelmRepos(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
tokenData, err := security.RetrieveTokenData(r)
diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go
index dd0bcd372..7c1364736 100644
--- a/api/http/handler/users/handler.go
+++ b/api/http/handler/users/handler.go
@@ -37,6 +37,7 @@ type Handler struct {
CryptoService portainer.CryptoService
passwordStrengthChecker security.PasswordStrengthChecker
AdminCreationDone chan<- struct{}
+ FileService portainer.FileService
}
// NewHandler creates a handler to manage user operations.
@@ -77,5 +78,10 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
publicRouter.Handle("/users/admin/check", httperror.LoggerHandler(h.adminCheck)).Methods(http.MethodGet)
publicRouter.Handle("/users/admin/init", httperror.LoggerHandler(h.adminInit)).Methods(http.MethodPost)
+ // Helm repositories
+ authenticatedRouter.Handle("/users/{id}/helm/repositories", httperror.LoggerHandler(h.userGetHelmRepos)).Methods(http.MethodGet)
+ authenticatedRouter.Handle("/users/{id}/helm/repositories", httperror.LoggerHandler(h.userCreateHelmRepo)).Methods(http.MethodPost)
+ authenticatedRouter.Handle("/users/{id}/helm/repositories/{repositoryID}", httperror.LoggerHandler(h.userDeleteHelmRepo)).Methods(http.MethodDelete)
+
return h
}
diff --git a/api/http/handler/users/user_helm_repos.go b/api/http/handler/users/user_helm_repos.go
new file mode 100644
index 000000000..e8490b2e0
--- /dev/null
+++ b/api/http/handler/users/user_helm_repos.go
@@ -0,0 +1,196 @@
+package users
+
+import (
+ "net/http"
+ "strings"
+
+ portainer "github.com/portainer/portainer/api"
+ httperrors "github.com/portainer/portainer/api/http/errors"
+ "github.com/portainer/portainer/api/http/security"
+ "github.com/portainer/portainer/pkg/libhelm"
+ httperror "github.com/portainer/portainer/pkg/libhttp/error"
+ "github.com/portainer/portainer/pkg/libhttp/request"
+ "github.com/portainer/portainer/pkg/libhttp/response"
+
+ "github.com/pkg/errors"
+)
+
+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, nil)
+}
+
+// @id HelmUserRepositoryCreate
+// @summary Create a user helm repository
+// @description Create a user helm repository.
+// @description **Access policy**: authenticated
+// @tags helm
+// @security ApiKeyAuth
+// @security jwt
+// @accept json
+// @produce json
+// @param id path int true "User identifier"
+// @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 /users/{id}/helm/repositories [post]
+func (handler *Handler) userCreateHelmRepo(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+ userIDEndpoint, err := request.RetrieveNumericRouteVariableValue(r, "id")
+ if err != nil {
+ return httperror.BadRequest("Invalid user identifier route variable", err)
+ }
+
+ tokenData, err := security.RetrieveTokenData(r)
+ if err != nil {
+ return httperror.InternalServerError("Unable to retrieve user authentication token", err)
+ }
+
+ userID := portainer.UserID(userIDEndpoint)
+ if tokenData.ID != userID {
+ return httperror.Forbidden("Couldn't create Helm repositories for another user", httperrors.ErrUnauthorized)
+ }
+
+ p := new(addHelmRepoUrlPayload)
+ err = request.DecodeAndValidateJSONPayload(r, p)
+ if err != nil {
+ return httperror.BadRequest("Invalid Helm repository URL", 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.InternalServerError("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.BadRequest(errMsg, errors.New(errMsg))
+ }
+ }
+
+ record := portainer.HelmUserRepository{
+ UserID: userID,
+ URL: p.URL,
+ }
+
+ err = handler.DataStore.HelmUserRepository().Create(&record)
+ if err != nil {
+ return httperror.InternalServerError("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 ApiKeyAuth
+// @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 /users/{id}/helm/repositories [get]
+func (handler *Handler) userGetHelmRepos(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+ userIDEndpoint, err := request.RetrieveNumericRouteVariableValue(r, "id")
+ if err != nil {
+ return httperror.BadRequest("Invalid user identifier route variable", err)
+ }
+
+ tokenData, err := security.RetrieveTokenData(r)
+ if err != nil {
+ return httperror.InternalServerError("Unable to retrieve user authentication token", err)
+ }
+
+ userID := portainer.UserID(userIDEndpoint)
+ if tokenData.ID != userID {
+ return httperror.Forbidden("Couldn't create Helm repositories for another user", httperrors.ErrUnauthorized)
+ }
+
+ settings, err := handler.DataStore.Settings().Settings()
+ if err != nil {
+ return httperror.InternalServerError("Unable to retrieve settings from the database", err)
+ }
+
+ userRepos, err := handler.DataStore.HelmUserRepository().HelmUserRepositoryByUserID(userID)
+ if err != nil {
+ return httperror.InternalServerError("Unable to get user Helm repositories", err)
+ }
+
+ resp := helmUserRepositoryResponse{
+ GlobalRepository: settings.HelmRepositoryURL,
+ UserRepositories: userRepos,
+ }
+
+ return response.JSON(w, resp)
+}
+
+// @id HelmUserRepositoryDelete
+// @summary Delete a users helm repositoryies
+// @description **Access policy**: authenticated
+// @tags helm
+// @security ApiKeyAuth
+// @security jwt
+// @produce json
+// @param id path int true "User identifier"
+// @param repositoryID path int true "Repository identifier"
+// @success 204 "Success"
+// @failure 400 "Invalid request"
+// @failure 403 "Permission denied"
+// @failure 500 "Server error"
+// @router /users/{id}/helm/repositories/{repositoryID} [delete]
+func (handler *Handler) userDeleteHelmRepo(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+ userIDEndpoint, err := request.RetrieveNumericRouteVariableValue(r, "id")
+ if err != nil {
+ return httperror.BadRequest("Invalid user identifier route variable", err)
+ }
+
+ tokenData, err := security.RetrieveTokenData(r)
+ if err != nil {
+ return httperror.InternalServerError("Unable to retrieve user authentication token", err)
+ }
+
+ userID := portainer.UserID(userIDEndpoint)
+ if tokenData.ID != userID {
+ return httperror.Forbidden("Couldn't create Helm repositories for another user", httperrors.ErrUnauthorized)
+ }
+
+ repositoryID, err := request.RetrieveNumericRouteVariableValue(r, "repositoryID")
+ if err != nil {
+ return httperror.BadRequest("Invalid user identifier route variable", err)
+ }
+
+ userRepos, err := handler.DataStore.HelmUserRepository().HelmUserRepositoryByUserID(userID)
+ if err != nil {
+ return httperror.InternalServerError("Unable to get user Helm repositories", err)
+ }
+
+ for _, repo := range userRepos {
+ if repo.ID == portainer.HelmUserRepositoryID(repositoryID) && repo.UserID == userID {
+ err = handler.DataStore.HelmUserRepository().Delete(portainer.HelmUserRepositoryID(repositoryID))
+ if err != nil {
+ return httperror.InternalServerError("Unable to delete user Helm repository", err)
+ }
+ }
+ }
+
+ return response.JSON(w, nil)
+}
diff --git a/api/http/server.go b/api/http/server.go
index 7800524f1..b14ecaf68 100644
--- a/api/http/server.go
+++ b/api/http/server.go
@@ -282,6 +282,7 @@ func (server *Server) Start() error {
userHandler.DataStore = server.DataStore
userHandler.CryptoService = server.CryptoService
userHandler.AdminCreationDone = server.AdminCreationDone
+ userHandler.FileService = server.FileService
var websocketHandler = websocket.NewHandler(server.KubernetesTokenCacheManager, requestBouncer)
websocketHandler.DataStore = server.DataStore
diff --git a/app/assets/ico/helm.svg b/app/assets/ico/helm.svg
index a6dcc4034..79558ad76 100644
--- a/app/assets/ico/helm.svg
+++ b/app/assets/ico/helm.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js
index a2e19f83c..249001f5d 100644
--- a/app/kubernetes/__module.js
+++ b/app/kubernetes/__module.js
@@ -327,7 +327,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
const deploy = {
name: 'kubernetes.deploy',
- url: '/deploy?templateId&referrer&tab',
+ url: '/deploy?templateId&referrer&tab&buildMethod&chartName',
views: {
'content@': {
component: 'kubernetesDeployView',
diff --git a/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.controller.js b/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.controller.js
deleted file mode 100644
index 373bfda48..000000000
--- a/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.controller.js
+++ /dev/null
@@ -1,39 +0,0 @@
-export default class HelmAddRepositoryController {
- /* @ngInject */
- constructor($state, $async, HelmService, Notifications) {
- this.$state = $state;
- this.$async = $async;
- this.HelmService = HelmService;
- this.Notifications = Notifications;
- }
-
- doesRepoExist() {
- if (!this.state.repository) {
- return false;
- }
- // lowercase, strip trailing slash and compare
- return this.repos.includes(this.state.repository.toLowerCase().replace(/\/$/, ''));
- }
-
- async addRepository() {
- this.state.isAddingRepo = true;
- try {
- await this.HelmService.addHelmRepository(this.endpoint.Id, { url: this.state.repository });
- this.Notifications.success('Success', 'Helm repository added successfully');
- this.$state.reload(this.$state.current);
- } catch (err) {
- this.Notifications.error('Installation error', err);
- } finally {
- this.state.isAddingRepo = false;
- }
- }
-
- $onInit() {
- return this.$async(async () => {
- this.state = {
- isAddingRepo: false,
- repository: '',
- };
- });
- }
-}
diff --git a/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.html b/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.html
deleted file mode 100644
index c79ab1c7e..000000000
--- a/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.html
+++ /dev/null
@@ -1,72 +0,0 @@
-
-