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>
This commit is contained in:
@@ -54,23 +54,27 @@ func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, helmPacka
|
||||
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, dataStore portainer.DataStore, helmPackageManager libhelm.HelmPackageManager) *Handler {
|
||||
func NewTemplateHandler(bouncer requestBouncer, helmPackageManager libhelm.HelmPackageManager) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
dataStore: dataStore,
|
||||
helmPackageManager: helmPackageManager,
|
||||
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}",
|
||||
// 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
|
||||
|
||||
@@ -17,7 +17,8 @@ import (
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param namespace
|
||||
// @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"
|
||||
|
||||
@@ -12,16 +12,14 @@ import (
|
||||
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/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
validation "github.com/portainer/portainer/api/kubernetes/validation"
|
||||
"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"`
|
||||
}
|
||||
|
||||
@@ -30,8 +28,49 @@ var errChartNameInvalid = errors.New("invalid chart name. " +
|
||||
" and must start and end with an alphanumeric character",
|
||||
)
|
||||
|
||||
// @id HelmInstall
|
||||
// @summary Install Helm Chart
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm_chart
|
||||
// @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/{release} [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")
|
||||
}
|
||||
@@ -52,75 +91,16 @@ func (p *installChartPayload) Validate(_ *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPayload(r *http.Request) (*installChartPayload, error) {
|
||||
p := new(installChartPayload)
|
||||
err := request.DecodeAndValidateJSONPayload(r, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*release.Release, error) {
|
||||
clusterAccess, httperr := handler.getHelmClusterAccess(r)
|
||||
if httperr != nil {
|
||||
return nil, httperr.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, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint on request context", err}
|
||||
}
|
||||
|
||||
bearerToken, err := security.ExtractBearerToken(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
|
||||
}
|
||||
|
||||
settings, err := handler.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve settings", Err: err}
|
||||
}
|
||||
|
||||
payload, err := readPayload(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "Invalid Helm install payload",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
release, err := handler.installChart(settings.HelmRepositoryURL, endpoint, payload, 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, bearerToken string) (*release.Release, error) {
|
||||
clusterAccess := handler.kubeConfigService.GetKubeConfigInternal(endpoint.ID, bearerToken)
|
||||
installOpts := options.InstallOptions{
|
||||
Name: p.Name,
|
||||
Chart: p.Chart,
|
||||
Namespace: p.Namespace,
|
||||
Repo: repo,
|
||||
Repo: p.Repo,
|
||||
KubernetesClusterAccess: &options.KubernetesClusterAccess{
|
||||
ClusterServerURL: clusterAccess.ClusterServerURL,
|
||||
CertificateAuthorityFile: clusterAccess.CertificateAuthorityFile,
|
||||
|
||||
@@ -38,11 +38,11 @@ func Test_helmInstall(t *testing.T) {
|
||||
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"}
|
||||
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", func(t *testing.T) {
|
||||
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)
|
||||
|
||||
@@ -17,10 +17,10 @@ import (
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param selector specify an optional selector
|
||||
// @param namespace specify an optional namespace
|
||||
// @param filter specify an optional filter
|
||||
// @success 200 {object} portainer.Helm "Success"
|
||||
// @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"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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"
|
||||
@@ -13,22 +16,28 @@ import (
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm_chart
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @success 200 {object} string "Success"
|
||||
// @failure 400 "Bad request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Endpoint or ServiceAccount not found"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /templates/helm [get]
|
||||
func (handler *Handler) helmRepoSearch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.dataStore.Settings().Settings()
|
||||
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.StatusInternalServerError, Message: "Unable to retrieve settings", Err: err}
|
||||
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: settings.HelmRepositoryURL,
|
||||
Repo: repo,
|
||||
}
|
||||
|
||||
result, err := libhelm.SearchRepo(searchOpts)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
@@ -16,23 +17,34 @@ import (
|
||||
func Test_helmRepoSearch(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
defaultSettings := &portainer.Settings{
|
||||
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
|
||||
}
|
||||
store := helper.NewDatastore(helper.WithSettingsService(defaultSettings))
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
h := NewTemplateHandler(helper.NewTestRequestBouncer(), store, helmPackageManager)
|
||||
h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager)
|
||||
|
||||
assert.NotNil(t, h, "Handler should not fail")
|
||||
|
||||
t.Run("helmRepoSearch", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/templates/helm", nil)
|
||||
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(rr.Code, http.StatusOK, "Status should be 200 OK")
|
||||
|
||||
_, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
is.Equal(http.StatusBadRequest, rr.Code, "Status should be 400 Bad request")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
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 HelmList
|
||||
// @summary List Helm Chart(s)
|
||||
// @id HelmShow
|
||||
// @summary Show Helm Chart(s)
|
||||
// @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
|
||||
@@ -21,20 +27,20 @@ import (
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Endpoint or ServiceAccount not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /templates/helm/{chart}/{command} [get]
|
||||
// @router /templates/helm/{command} [get]
|
||||
func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.dataStore.Settings().Settings()
|
||||
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.StatusInternalServerError, Message: "Unable to retrieve settings", Err: err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo))}
|
||||
}
|
||||
|
||||
chart, err := request.RetrieveRouteVariableValue(r, "chart")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Missing chart name for show",
|
||||
Err: err,
|
||||
}
|
||||
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")
|
||||
@@ -46,7 +52,7 @@ func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httper
|
||||
showOptions := options.ShowOptions{
|
||||
OutputFormat: options.ShowOutputFormat(cmd),
|
||||
Chart: chart,
|
||||
Repo: settings.HelmRepositoryURL,
|
||||
Repo: repo,
|
||||
}
|
||||
result, err := handler.helmPackageManager.Show(showOptions)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -16,12 +16,8 @@ import (
|
||||
func Test_helmShow(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
defaultSettings := &portainer.Settings{
|
||||
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
|
||||
}
|
||||
store := helper.NewDatastore(helper.WithSettingsService(defaultSettings))
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
h := NewTemplateHandler(helper.NewTestRequestBouncer(), store, helmPackageManager)
|
||||
h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
@@ -31,12 +27,13 @@ func Test_helmShow(t *testing.T) {
|
||||
"readme": test.MockDataReadme,
|
||||
}
|
||||
|
||||
chartName := "test-nginx"
|
||||
for cmd, expect := range commands {
|
||||
t.Run(cmd, func(t *testing.T) {
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/templates/helm/%s/%s", chartName, cmd), nil)
|
||||
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)
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
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 {
|
||||
GlobalRepo string `json:"GlobalRepo"`
|
||||
UserRepos []portainer.HelmUserRepository `json:"UserRepos"`
|
||||
}
|
||||
|
||||
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 users
|
||||
// @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,
|
||||
}
|
||||
}
|
||||
|
||||
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, suffix trimmed comparison
|
||||
for _, record := range records {
|
||||
if strings.EqualFold(strings.TrimSuffix(record.URL, "/"), strings.TrimSuffix(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 users
|
||||
// @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{
|
||||
GlobalRepo: settings.HelmRepositoryURL,
|
||||
UserRepos: userRepos,
|
||||
}
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -50,8 +52,11 @@ 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.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)
|
||||
@@ -107,7 +112,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
if payload.HelmRepositoryURL != nil {
|
||||
settings.HelmRepositoryURL = *payload.HelmRepositoryURL
|
||||
settings.HelmRepositoryURL = strings.TrimSuffix(*payload.HelmRepositoryURL, "/")
|
||||
}
|
||||
|
||||
if payload.BlackListedLabels != nil {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user