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:
zees-dev
2021-09-06 11:36:04 +12:00
committed by zees-dev
parent aa9fe5d2d7
commit c2f079b4e3
36 changed files with 683 additions and 247 deletions
+9 -5
View File
@@ -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
+2 -1
View File
@@ -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"
+48 -68
View File
@@ -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,
+2 -2
View File
@@ -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)
+4 -4
View File
@@ -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"
+14 -5
View File
@@ -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)
+24 -12
View File
@@ -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")
})
}
+19 -13
View File
@@ -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 -8
View File
@@ -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)
+124
View File
@@ -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)
}
+9 -4
View File
@@ -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 {
+1 -1
View File
@@ -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"