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:
+30
-27
@@ -6,6 +6,8 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt/helmuserrepository"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/customtemplate"
|
||||
@@ -44,33 +46,34 @@ const (
|
||||
// Store defines the implementation of portainer.DataStore using
|
||||
// BoltDB as the storage system.
|
||||
type Store struct {
|
||||
path string
|
||||
connection *internal.DbConnection
|
||||
isNew bool
|
||||
fileService portainer.FileService
|
||||
CustomTemplateService *customtemplate.Service
|
||||
DockerHubService *dockerhub.Service
|
||||
EdgeGroupService *edgegroup.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
EdgeStackService *edgestack.Service
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
ExtensionService *extension.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
RoleService *role.Service
|
||||
ScheduleService *schedule.Service
|
||||
SettingsService *settings.Service
|
||||
SSLSettingsService *ssl.Service
|
||||
StackService *stack.Service
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TeamService *team.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
path string
|
||||
connection *internal.DbConnection
|
||||
isNew bool
|
||||
fileService portainer.FileService
|
||||
CustomTemplateService *customtemplate.Service
|
||||
DockerHubService *dockerhub.Service
|
||||
EdgeGroupService *edgegroup.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
EdgeStackService *edgestack.Service
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
ExtensionService *extension.Service
|
||||
HelmUserRepositoryService *helmuserrepository.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
RoleService *role.Service
|
||||
ScheduleService *schedule.Service
|
||||
SettingsService *settings.Service
|
||||
SSLSettingsService *ssl.Service
|
||||
StackService *stack.Service
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TeamService *team.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
}
|
||||
|
||||
func (store *Store) edition() portainer.SoftwareEdition {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package helmuserrepository
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "helm_user_repository"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HelmUserRepositoryByUserID return an array containing all the HelmUserRepository objects where the specified userID is present.
|
||||
func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error) {
|
||||
var result = make([]portainer.HelmUserRepository, 0)
|
||||
|
||||
err := service.connection.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var record portainer.HelmUserRepository
|
||||
err := internal.UnmarshalObject(v, &record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record.UserID == userID {
|
||||
result = append(result, record)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// CreateHelmUserRepository creates a new HelmUserRepository object.
|
||||
func (service *Service) CreateHelmUserRepository(record *portainer.HelmUserRepository) error {
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
record.ID = portainer.HelmUserRepositoryID(id)
|
||||
|
||||
data, err := internal.MarshalObject(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(record.ID)), data)
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/endpointgroup"
|
||||
"github.com/portainer/portainer/api/bolt/endpointrelation"
|
||||
"github.com/portainer/portainer/api/bolt/extension"
|
||||
"github.com/portainer/portainer/api/bolt/helmuserrepository"
|
||||
"github.com/portainer/portainer/api/bolt/registry"
|
||||
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
||||
"github.com/portainer/portainer/api/bolt/role"
|
||||
@@ -88,6 +89,12 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.ExtensionService = extensionService
|
||||
|
||||
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.HelmUserRepositoryService = helmUserRepositoryService
|
||||
|
||||
registryService, err := registry.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -204,6 +211,11 @@ func (store *Store) EndpointRelation() portainer.EndpointRelationService {
|
||||
return store.EndpointRelationService
|
||||
}
|
||||
|
||||
// HelmUserRepository access the helm user repository settings
|
||||
func (store *Store) HelmUserRepository() portainer.HelmUserRepositoryService {
|
||||
return store.HelmUserRepositoryService
|
||||
}
|
||||
|
||||
// Registry gives access to the Registry data management layer
|
||||
func (store *Store) Registry() portainer.RegistryService {
|
||||
return store.RegistryService
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210827003723-50899dcffbec
|
||||
github.com/portainer/libhelm v0.0.0-20210903050903-43c3353fd37c
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
|
||||
+2
-2
@@ -210,8 +210,8 @@ github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548 h
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhelm v0.0.0-20210827003723-50899dcffbec h1:L3o94L7VYLjAt4gKGKpgKFIkzpUYPh/UiEVSyjNGYGE=
|
||||
github.com/portainer/libhelm v0.0.0-20210827003723-50899dcffbec/go.mod h1:dL39owjMIeGKuSsSbbTrLCzZQfoB1zORhq0TAet7D5E=
|
||||
github.com/portainer/libhelm v0.0.0-20210903050903-43c3353fd37c h1:KALKyW7pC75SBMpLgB0zL129OwHxYwYxVc6x0HjPFD0=
|
||||
github.com/portainer/libhelm v0.0.0-20210903050903-43c3353fd37c/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
|
||||
@@ -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"
|
||||
|
||||
+3
-3
@@ -27,7 +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"
|
||||
helmhandler "github.com/portainer/portainer/api/http/handler/helm"
|
||||
"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"
|
||||
@@ -170,9 +170,9 @@ func (server *Server) Start() error {
|
||||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||
|
||||
var endpointHelmHandler = helmhandler.NewHandler(requestBouncer, server.DataStore, server.HelmPackageManager, server.KubeConfigService)
|
||||
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.HelmPackageManager, server.KubeConfigService)
|
||||
|
||||
var helmTemplatesHandler = helmhandler.NewTemplateHandler(requestBouncer, server.DataStore, server.HelmPackageManager)
|
||||
var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager)
|
||||
|
||||
var motdHandler = motd.NewHandler(requestBouncer)
|
||||
|
||||
|
||||
@@ -7,26 +7,27 @@ import (
|
||||
)
|
||||
|
||||
type datastore struct {
|
||||
customTemplate portainer.CustomTemplateService
|
||||
edgeGroup portainer.EdgeGroupService
|
||||
edgeJob portainer.EdgeJobService
|
||||
edgeStack portainer.EdgeStackService
|
||||
endpoint portainer.EndpointService
|
||||
endpointGroup portainer.EndpointGroupService
|
||||
endpointRelation portainer.EndpointRelationService
|
||||
registry portainer.RegistryService
|
||||
resourceControl portainer.ResourceControlService
|
||||
role portainer.RoleService
|
||||
sslSettings portainer.SSLSettingsService
|
||||
settings portainer.SettingsService
|
||||
stack portainer.StackService
|
||||
tag portainer.TagService
|
||||
teamMembership portainer.TeamMembershipService
|
||||
team portainer.TeamService
|
||||
tunnelServer portainer.TunnelServerService
|
||||
user portainer.UserService
|
||||
version portainer.VersionService
|
||||
webhook portainer.WebhookService
|
||||
customTemplate portainer.CustomTemplateService
|
||||
edgeGroup portainer.EdgeGroupService
|
||||
edgeJob portainer.EdgeJobService
|
||||
edgeStack portainer.EdgeStackService
|
||||
endpoint portainer.EndpointService
|
||||
endpointGroup portainer.EndpointGroupService
|
||||
endpointRelation portainer.EndpointRelationService
|
||||
helmUserRepository portainer.HelmUserRepositoryService
|
||||
registry portainer.RegistryService
|
||||
resourceControl portainer.ResourceControlService
|
||||
role portainer.RoleService
|
||||
sslSettings portainer.SSLSettingsService
|
||||
settings portainer.SettingsService
|
||||
stack portainer.StackService
|
||||
tag portainer.TagService
|
||||
teamMembership portainer.TeamMembershipService
|
||||
team portainer.TeamService
|
||||
tunnelServer portainer.TunnelServerService
|
||||
user portainer.UserService
|
||||
version portainer.VersionService
|
||||
webhook portainer.WebhookService
|
||||
}
|
||||
|
||||
func (d *datastore) BackupTo(io.Writer) error { return nil }
|
||||
@@ -44,19 +45,22 @@ func (d *datastore) EdgeStack() portainer.EdgeStackService { retur
|
||||
func (d *datastore) Endpoint() portainer.EndpointService { return d.endpoint }
|
||||
func (d *datastore) EndpointGroup() portainer.EndpointGroupService { return d.endpointGroup }
|
||||
func (d *datastore) EndpointRelation() portainer.EndpointRelationService { return d.endpointRelation }
|
||||
func (d *datastore) Registry() portainer.RegistryService { return d.registry }
|
||||
func (d *datastore) ResourceControl() portainer.ResourceControlService { return d.resourceControl }
|
||||
func (d *datastore) Role() portainer.RoleService { return d.role }
|
||||
func (d *datastore) Settings() portainer.SettingsService { return d.settings }
|
||||
func (d *datastore) SSLSettings() portainer.SSLSettingsService { return d.sslSettings }
|
||||
func (d *datastore) Stack() portainer.StackService { return d.stack }
|
||||
func (d *datastore) Tag() portainer.TagService { return d.tag }
|
||||
func (d *datastore) TeamMembership() portainer.TeamMembershipService { return d.teamMembership }
|
||||
func (d *datastore) Team() portainer.TeamService { return d.team }
|
||||
func (d *datastore) TunnelServer() portainer.TunnelServerService { return d.tunnelServer }
|
||||
func (d *datastore) User() portainer.UserService { return d.user }
|
||||
func (d *datastore) Version() portainer.VersionService { return d.version }
|
||||
func (d *datastore) Webhook() portainer.WebhookService { return d.webhook }
|
||||
func (d *datastore) HelmUserRepository() portainer.HelmUserRepositoryService {
|
||||
return d.helmUserRepository
|
||||
}
|
||||
func (d *datastore) Registry() portainer.RegistryService { return d.registry }
|
||||
func (d *datastore) ResourceControl() portainer.ResourceControlService { return d.resourceControl }
|
||||
func (d *datastore) Role() portainer.RoleService { return d.role }
|
||||
func (d *datastore) Settings() portainer.SettingsService { return d.settings }
|
||||
func (d *datastore) SSLSettings() portainer.SSLSettingsService { return d.sslSettings }
|
||||
func (d *datastore) Stack() portainer.StackService { return d.stack }
|
||||
func (d *datastore) Tag() portainer.TagService { return d.tag }
|
||||
func (d *datastore) TeamMembership() portainer.TeamMembershipService { return d.teamMembership }
|
||||
func (d *datastore) Team() portainer.TeamService { return d.team }
|
||||
func (d *datastore) TunnelServer() portainer.TunnelServerService { return d.tunnelServer }
|
||||
func (d *datastore) User() portainer.UserService { return d.user }
|
||||
func (d *datastore) Version() portainer.VersionService { return d.version }
|
||||
func (d *datastore) Webhook() portainer.WebhookService { return d.webhook }
|
||||
|
||||
type datastoreOption = func(d *datastore)
|
||||
|
||||
|
||||
@@ -389,6 +389,18 @@ type (
|
||||
ProjectPath string `json:"ProjectPath"`
|
||||
}
|
||||
|
||||
HelmUserRepositoryID int
|
||||
|
||||
// HelmUserRepositories stores a Helm repository URL for the given user
|
||||
HelmUserRepository struct {
|
||||
// Membership Identifier
|
||||
ID HelmUserRepositoryID `json:"Id" example:"1"`
|
||||
// User identifier
|
||||
UserID UserID `json:"UserID" example:"1"`
|
||||
// Helm repository URL
|
||||
URL string `json:"URL" example:"https://charts.bitnami.com/bitnami"`
|
||||
}
|
||||
|
||||
// QuayRegistryData represents data required for Quay registry to work
|
||||
QuayRegistryData struct {
|
||||
UseOrganisation bool `json:"UseOrganisation"`
|
||||
@@ -1093,6 +1105,7 @@ type (
|
||||
Endpoint() EndpointService
|
||||
EndpointGroup() EndpointGroupService
|
||||
EndpointRelation() EndpointRelationService
|
||||
HelmUserRepository() HelmUserRepositoryService
|
||||
Registry() RegistryService
|
||||
ResourceControl() ResourceControlService
|
||||
Role() RoleService
|
||||
@@ -1220,6 +1233,12 @@ type (
|
||||
LatestCommitID(repositoryURL, referenceName, username, password string) (string, error)
|
||||
}
|
||||
|
||||
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
|
||||
HelmUserRepositoryService interface {
|
||||
HelmUserRepositoryByUserID(userID UserID) ([]HelmUserRepository, error)
|
||||
CreateHelmUserRepository(record *HelmUserRepository) error
|
||||
}
|
||||
|
||||
// JWTService represents a service for managing JWT tokens
|
||||
JWTService interface {
|
||||
GenerateToken(data *TokenData) (string, error)
|
||||
|
||||
@@ -289,6 +289,10 @@ a[ng-click] {
|
||||
padding-top: 15px !important;
|
||||
}
|
||||
|
||||
.nomargin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
export default class HelmAddRepositoryController {
|
||||
/* @ngInject */
|
||||
constructor($async, $window, $analytics, HelmService, Notifications, EndpointProvider) {
|
||||
this.$async = $async;
|
||||
this.$window = $window;
|
||||
this.$analytics = $analytics;
|
||||
this.HelmService = HelmService;
|
||||
this.Notifications = Notifications;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
}
|
||||
|
||||
async addRepository() {
|
||||
this.state.isAddingRepo = true;
|
||||
try {
|
||||
const { URL } = await this.HelmService.addHelmRepository(this.EndpointProvider.currentEndpoint().Id, { url: this.state.repository });
|
||||
this.Notifications.success('Helm repository added successfully');
|
||||
this.refreshCharts([URL], true);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Installation error', err);
|
||||
} finally {
|
||||
this.state.isAddingRepo = false;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.state = {
|
||||
isAddingRepo: false,
|
||||
repository: '',
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-dharmachakra" title-text="Additional repositories"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="actionBar">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Add a Helm repository. All Helm charts in the repository will be added to the list.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<input type="text" name="repo" class="form-control" ng-model="$ctrl.state.repository" placeholder="https://charts.bitnami.com/bitnami" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-default nomargin"
|
||||
ng-click="$ctrl.addRepository()"
|
||||
ng-disabled="$ctrl.state.isAddingRepo"
|
||||
analytics-on
|
||||
analytics-category="kubernetes"
|
||||
analytics-event="kubernetes-helm-add-repository"
|
||||
>
|
||||
Add repository
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import angular from 'angular';
|
||||
import controller from './helm-add-repository.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('helmAddRepository', {
|
||||
templateUrl: './helm-add-repository.html',
|
||||
controller,
|
||||
bindings: {
|
||||
charts: '@',
|
||||
refreshCharts: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
+2
-2
@@ -11,7 +11,7 @@ export default class HelmTemplatesListController {
|
||||
|
||||
async updateCategories() {
|
||||
try {
|
||||
const annotationCategories = this.templates
|
||||
const annotationCategories = this.charts
|
||||
.map((t) => t.annotations) // get annotations
|
||||
.filter((a) => a) // filter out undefined/nulls
|
||||
.map((c) => c.category); // get annotation category
|
||||
@@ -31,7 +31,7 @@ export default class HelmTemplatesListController {
|
||||
}
|
||||
|
||||
$onChanges() {
|
||||
if (this.templates.length > 0) {
|
||||
if (this.charts.length > 0) {
|
||||
this.updateCategories();
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -36,8 +36,8 @@
|
||||
|
||||
<div class="blocklist">
|
||||
<helm-templates-list-item
|
||||
ng-repeat="template in $ctrl.templates | filter:$ctrl.state.textFilter | filter: $ctrl.state.selectedCategory "
|
||||
model="template"
|
||||
ng-repeat="chart in $ctrl.charts | filter:$ctrl.state.textFilter | filter: $ctrl.state.selectedCategory "
|
||||
model="chart"
|
||||
type-label="helm"
|
||||
on-select="($ctrl.selectAction)"
|
||||
>
|
||||
@@ -48,7 +48,7 @@
|
||||
Initial download of Helm Charts can take a few minutes
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="!$ctrl.loading && $ctrl.templates.length === 0" class="text-center text-muted">
|
||||
<div ng-if="!$ctrl.loading && $ctrl.charts.length === 0" class="text-center text-muted">
|
||||
No helm charts available.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ angular.module('portainer.kubernetes').component('helmTemplatesList', {
|
||||
loading: '<',
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
templates: '<',
|
||||
charts: '<',
|
||||
tableKey: '@',
|
||||
selectAction: '<',
|
||||
},
|
||||
|
||||
@@ -2,12 +2,13 @@ import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
|
||||
export default class HelmTemplatesController {
|
||||
/* @ngInject */
|
||||
constructor($analytics, $window, $async, $state, $anchorScroll, HelmService, KubernetesResourcePoolService, Notifications, ModalService) {
|
||||
constructor($analytics, $async, $state, $window, $anchorScroll, Authentication, HelmService, KubernetesResourcePoolService, Notifications, ModalService) {
|
||||
this.$analytics = $analytics;
|
||||
this.$window = $window;
|
||||
this.$async = $async;
|
||||
this.$window = $window;
|
||||
this.$state = $state;
|
||||
this.$anchorScroll = $anchorScroll;
|
||||
this.Authentication = Authentication;
|
||||
this.HelmService = HelmService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.Notifications = Notifications;
|
||||
@@ -16,9 +17,11 @@ export default class HelmTemplatesController {
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.uiCanExit = this.uiCanExit.bind(this);
|
||||
this.installHelmchart = this.installHelmchart.bind(this);
|
||||
this.loadInitialData = this.loadInitialData.bind(this);
|
||||
this.getHelmValues = this.getHelmValues.bind(this);
|
||||
this.selectHelmChart = this.selectHelmChart.bind(this);
|
||||
this.getHelmRepoURLs = this.getHelmRepoURLs.bind(this);
|
||||
this.getLatestCharts = this.getLatestCharts.bind(this);
|
||||
this.getResourcePools = this.getResourcePools.bind(this);
|
||||
|
||||
$window.onbeforeunload = () => {
|
||||
if (this.state.isEditorDirty) {
|
||||
@@ -46,9 +49,16 @@ export default class HelmTemplatesController {
|
||||
async installHelmchart() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
await this.HelmService.install(this.state.appName, this.state.resourcePool.Namespace.Name, this.state.template.name, this.state.values);
|
||||
const payload = {
|
||||
Name: this.state.appName,
|
||||
Repo: this.state.chart.repo,
|
||||
Chart: this.state.chart.name,
|
||||
Values: this.state.values,
|
||||
Namespace: this.state.resourcePool.Namespace.Name,
|
||||
};
|
||||
await this.HelmService.install(this.endpoint.Id, payload);
|
||||
this.Notifications.success('Helm Chart successfully installed');
|
||||
this.$analytics.eventTrack('kubernetes-helm-install', { category: 'kubernetes', metadata: { 'chart-name': this.state.template.name } });
|
||||
this.$analytics.eventTrack('kubernetes-helm-install', { category: 'kubernetes', metadata: { 'chart-name': this.state.chart.name } });
|
||||
this.state.isEditorDirty = false;
|
||||
this.$state.go('kubernetes.applications');
|
||||
} catch (err) {
|
||||
@@ -61,7 +71,7 @@ export default class HelmTemplatesController {
|
||||
async getHelmValues() {
|
||||
this.state.loadingValues = true;
|
||||
try {
|
||||
const { values } = await this.HelmService.values(this.state.template.name);
|
||||
const { values } = await this.HelmService.values(this.state.chart.repo, this.state.chart.name);
|
||||
this.state.values = values;
|
||||
this.state.originalvalues = values;
|
||||
} catch (err) {
|
||||
@@ -71,28 +81,73 @@ export default class HelmTemplatesController {
|
||||
}
|
||||
}
|
||||
|
||||
async selectHelmChart(template) {
|
||||
async selectHelmChart(chart) {
|
||||
this.$anchorScroll('view-top');
|
||||
this.state.showCustomValues = false;
|
||||
this.state.template = template;
|
||||
this.state.chart = chart;
|
||||
await this.getHelmValues();
|
||||
}
|
||||
|
||||
async loadInitialData() {
|
||||
this.state.templatesLoading = true;
|
||||
/**
|
||||
* @description This function is used to get the helm repo urls for the endpoint and user
|
||||
* @returns {Promise<string[]>} list of helm repo urls
|
||||
*/
|
||||
|
||||
async getHelmRepoURLs() {
|
||||
this.state.reposLoading = true;
|
||||
try {
|
||||
const [resourcePools, templates] = await Promise.all([this.KubernetesResourcePoolService.get(), this.HelmService.search()]);
|
||||
// fetch globally set helm repo and user helm repos (parallel)
|
||||
const { GlobalRepo, UserRepos } = await this.HelmService.getHelmRepositories(this.endpoint.Id);
|
||||
const userHelmReposUrls = UserRepos.map((repo) => repo.URL);
|
||||
const uniqueHelmRepos = [...new Set([GlobalRepo, ...userHelmReposUrls])]; // remove duplicates
|
||||
return uniqueHelmRepos;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm repo urls.');
|
||||
} finally {
|
||||
this.state.reposLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This function is used to fetch the respective index.yaml files for the provided helm repo urls
|
||||
* @param {string[]} helmRepos list of helm repositories
|
||||
* @param {bool} append append charts returned from repo to existing list of helm charts
|
||||
*/
|
||||
|
||||
async getLatestCharts(helmRepos, append = false) {
|
||||
this.state.chartsLoading = true;
|
||||
try {
|
||||
const promiseList = helmRepos.map((repo) => this.HelmService.search(repo));
|
||||
// fetch helm charts from all the provided helm repositories (parallel)
|
||||
// Promise.allSettled is used to account for promise failure(s) - in cases the user has provided invalid helm repo
|
||||
const chartPromises = await Promise.allSettled(promiseList);
|
||||
const latestCharts = chartPromises
|
||||
.filter((tp) => tp.status === 'fulfilled') // remove failed promises
|
||||
.map((tp) => ({ entries: tp.value.entries, repo: helmRepos[chartPromises.indexOf(tp)] })) // extract chart entries with respective repo data
|
||||
.flatMap(
|
||||
({ entries, repo }) => Object.values(entries).map((charts) => ({ ...charts[0], repo })) // flatten chart entries to single array with respective repo
|
||||
);
|
||||
|
||||
this.state.charts = append ? this.state.charts.concat(latestCharts) : latestCharts;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm repo charts.');
|
||||
} finally {
|
||||
this.state.chartsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async getResourcePools() {
|
||||
this.state.resourcePoolsLoading = true;
|
||||
try {
|
||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
|
||||
const nonSystemNamespaces = resourcePools.filter((resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
this.state.resourcePools = nonSystemNamespaces;
|
||||
this.state.resourcePool = nonSystemNamespaces[0];
|
||||
|
||||
const latestTemplates = Object.values(templates.entries).map((charts) => charts[0]);
|
||||
this.state.templates = latestTemplates;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve initial helm data.');
|
||||
} finally {
|
||||
this.state.templatesLoading = false;
|
||||
this.state.resourcePoolsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,21 +155,23 @@ export default class HelmTemplatesController {
|
||||
return this.$async(async () => {
|
||||
this.state = {
|
||||
appName: '',
|
||||
template: null,
|
||||
chart: null,
|
||||
showCustomValues: false,
|
||||
actionInProgress: false,
|
||||
resourcePools: [],
|
||||
resourcePool: '',
|
||||
values: null,
|
||||
originalvalues: null,
|
||||
templates: [],
|
||||
charts: [],
|
||||
loadingValues: false,
|
||||
isEditorDirty: false,
|
||||
|
||||
chartsLoading: false,
|
||||
resourcePoolsLoading: false,
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
await this.loadInitialData();
|
||||
const helmRepos = await this.getHelmRepoURLs();
|
||||
await Promise.all([this.getLatestCharts(helmRepos), this.getResourcePools()]);
|
||||
|
||||
this.state.viewReady = true;
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<rd-header-content>Charts</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<information-panel title-text="Information" ng-if="!$ctrl.state.template">
|
||||
<information-panel title-text="Information" ng-if="!$ctrl.state.chart">
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
<div class="row">
|
||||
<!-- helmchart-form -->
|
||||
<div class="col-sm-12" ng-if="$ctrl.state.template">
|
||||
<div class="col-sm-12" ng-if="$ctrl.state.chart">
|
||||
<rd-widget>
|
||||
<rd-widget-custom-header icon="$ctrl.state.template.icon" title-text="$ctrl.state.template.name"></rd-widget-custom-header>
|
||||
<rd-widget-custom-header icon="$ctrl.state.chart.icon" title-text="$ctrl.state.chart.name"></rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal" name="$ctrl.helmTemplateCreationForm">
|
||||
<!-- description -->
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<div class="template-note" ng-bind-html="$ctrl.state.template.description"></div>
|
||||
<div class="template-note" ng-bind-html="$ctrl.state.chart.description"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +148,7 @@
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Install</span>
|
||||
<span ng-hide="!$ctrl.state.actionInProgress">Helm installing in progress</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="$ctrl.state.template = null">Hide</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="$ctrl.state.chart = null">Hide</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !helm actions -->
|
||||
@@ -159,16 +159,22 @@
|
||||
<!-- helmchart-form -->
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<helm-add-repository charts="$ctrl.state.charts" refresh-charts="$ctrl.getLatestCharts"></helm-add-repository>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Helm Charts Component -->
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<helm-templates-list
|
||||
title-text="Charts"
|
||||
title-icon="fa-rocket"
|
||||
templates="$ctrl.state.templates"
|
||||
table-key="$ctrl.state.templates"
|
||||
charts="$ctrl.state.charts"
|
||||
table-key="$ctrl.state.charts"
|
||||
select-action="$ctrl.selectHelmChart"
|
||||
loading="$ctrl.state.templatesLoading"
|
||||
loading="$ctrl.state.chartsLoading || $ctrl.state.resourcePoolsLoading"
|
||||
>
|
||||
</helm-templates-list>
|
||||
</div>
|
||||
|
||||
@@ -4,4 +4,7 @@ import controller from './helm-templates.controller';
|
||||
angular.module('portainer.kubernetes').component('helmTemplatesView', {
|
||||
templateUrl: './helm-templates.html',
|
||||
controller,
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,31 +2,41 @@ import angular from 'angular';
|
||||
|
||||
angular.module('portainer.kubernetes').factory('HelmFactory', HelmFactory);
|
||||
|
||||
function HelmFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
/* @ngInject */
|
||||
function HelmFactory($resource, API_ENDPOINT_ENDPOINTS) {
|
||||
const helmUrl = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/helm';
|
||||
const templatesUrl = '/api/templates/helm';
|
||||
|
||||
return $resource(
|
||||
helmUrl,
|
||||
{
|
||||
endpointId: EndpointProvider.endpointID,
|
||||
},
|
||||
{},
|
||||
{
|
||||
templates: {
|
||||
url: templatesUrl,
|
||||
method: 'GET',
|
||||
params: { repo: '@repo' },
|
||||
cache: true,
|
||||
},
|
||||
show: {
|
||||
url: `${templatesUrl}/:chart/:type`,
|
||||
url: `${templatesUrl}/:type`,
|
||||
method: 'GET',
|
||||
params: { repo: '@repo', chart: '@chart' },
|
||||
transformResponse: function (data) {
|
||||
return { values: data };
|
||||
},
|
||||
},
|
||||
getHelmRepositories: {
|
||||
method: 'GET',
|
||||
url: `${helmUrl}/repositories`,
|
||||
},
|
||||
addHelmRepository: {
|
||||
method: 'POST',
|
||||
url: `${helmUrl}/repositories`,
|
||||
},
|
||||
list: {
|
||||
method: 'GET',
|
||||
isArray: true,
|
||||
params: { namespace: '@namespace', selector: '@selector', filter: '@filter', output: '@output' },
|
||||
},
|
||||
install: { method: 'POST' },
|
||||
uninstall: {
|
||||
|
||||
@@ -4,10 +4,12 @@ import PortainerError from 'Portainer/error';
|
||||
angular.module('portainer.kubernetes').factory('HelmService', HelmService);
|
||||
|
||||
/* @ngInject */
|
||||
export function HelmService(HelmFactory, EndpointProvider) {
|
||||
export function HelmService(HelmFactory) {
|
||||
return {
|
||||
search,
|
||||
values,
|
||||
getHelmRepositories,
|
||||
addHelmRepository,
|
||||
install,
|
||||
uninstall,
|
||||
listReleases,
|
||||
@@ -15,12 +17,13 @@ export function HelmService(HelmFactory, EndpointProvider) {
|
||||
|
||||
/**
|
||||
* @description: Searches for all helm charts in a helm repo
|
||||
* @param {string} repo - repo url to search charts for
|
||||
* @returns {Promise} - Resolves with `index.yaml` of helm charts for a repo
|
||||
* @throws {PortainerError} - Rejects with error if searching for the `index.yaml` fails
|
||||
*/
|
||||
async function search() {
|
||||
async function search(repo) {
|
||||
try {
|
||||
return await HelmFactory.templates().$promise;
|
||||
return await HelmFactory.templates({ repo }).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve helm charts', err);
|
||||
}
|
||||
@@ -28,36 +31,55 @@ export function HelmService(HelmFactory, EndpointProvider) {
|
||||
|
||||
/**
|
||||
* @description: Show values helm of a helm chart, this basically runs `helm show values`
|
||||
* @param {string} repo - repo url to search charts values for
|
||||
* @param {string} chart - chart within the repo to retrieve default values
|
||||
* @returns {Promise} - Resolves with `values.yaml` of helm chart values for a repo
|
||||
* @throws {PortainerError} - Rejects with error if helm show fails
|
||||
*/
|
||||
async function values(chart) {
|
||||
async function values(repo, chart) {
|
||||
try {
|
||||
return await HelmFactory.show({ chart, type: 'values' }).$promise;
|
||||
return await HelmFactory.show({ repo, chart, type: 'values' }).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve values from chart', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Show values helm of a helm chart, this basically runs `helm show values`
|
||||
* @returns {Promise} - Resolves with an object containing list of user helm repos and default/global settings helm repo
|
||||
* @throws {PortainerError} - Rejects with error if helm show fails
|
||||
*/
|
||||
async function getHelmRepositories(endpointId) {
|
||||
return await HelmFactory.getHelmRepositories({ endpointId }).$promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Adds a helm repo for the calling user
|
||||
* @param {Object} payload - helm repo url to add for the user
|
||||
* @returns {Promise} - Resolves with `values.yaml` of helm chart values for a repo
|
||||
* @throws {PortainerError} - Rejects with error if helm show fails
|
||||
*/
|
||||
async function addHelmRepository(endpointId, payload) {
|
||||
return await HelmFactory.addHelmRepository({ endpointId }, payload).$promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Installs a helm chart, this basically runs `helm install`
|
||||
* @returns {Promise} - Resolves with `values.yaml` of helm chart values for a repo
|
||||
* @throws {PortainerError} - Rejects with error if helm show fails
|
||||
*/
|
||||
async function install(appname, namespace, chart, values) {
|
||||
const endpointId = EndpointProvider.currentEndpoint().Id;
|
||||
const payload = {
|
||||
Name: appname,
|
||||
Namespace: namespace,
|
||||
Chart: chart,
|
||||
Values: values,
|
||||
};
|
||||
async function install(endpointId, payload) {
|
||||
return await HelmFactory.install({ endpointId }, payload).$promise;
|
||||
}
|
||||
|
||||
async function uninstall({ Name }) {
|
||||
/**
|
||||
* @description: Uninstall a helm chart, this basically runs `helm uninstall`
|
||||
* @param {Object} options - Options object, release `Name` is the only required option
|
||||
* @throws {PortainerError} - Rejects with error if helm show fails
|
||||
*/
|
||||
async function uninstall(endpointId, { Name }) {
|
||||
try {
|
||||
await HelmFactory.uninstall({ release: Name }).$promise;
|
||||
await HelmFactory.uninstall({ endpointId, release: Name }).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to delete release', err);
|
||||
}
|
||||
@@ -65,13 +87,13 @@ export function HelmService(HelmFactory, EndpointProvider) {
|
||||
|
||||
/**
|
||||
* @description: List all helm releases based on passed in options, this basically runs `helm list`
|
||||
* @param {object} options - Supported CLI flags to pass to Helm (binary) - flags to `helm list`
|
||||
* @param {Object} options - Supported CLI flags to pass to Helm (binary) - flags to `helm list`
|
||||
* @returns {Promise} - Resolves with list of helm releases
|
||||
* @throws {PortainerError} - Rejects with error if helm list fails
|
||||
*/
|
||||
async function listReleases({ namespace, selector, filter, output }) {
|
||||
async function listReleases(endpointId, { namespace, selector, filter, output }) {
|
||||
try {
|
||||
const releases = await HelmFactory.list({ selector, namespace, filter, output }).$promise;
|
||||
const releases = await HelmFactory.list({ endpointId, selector, namespace, filter, output }).$promise;
|
||||
return releases;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve release list', err);
|
||||
|
||||
@@ -4,5 +4,6 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsView', {
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ class KubernetesApplicationsController {
|
||||
for (const application of selectedItems) {
|
||||
try {
|
||||
if (application.ApplicationType === KubernetesApplicationTypes.HELM) {
|
||||
await this.HelmService.uninstall(application);
|
||||
await this.HelmService.uninstall(this.endpoint.Id, application);
|
||||
} else {
|
||||
await this.KubernetesApplicationService.delete(application);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default class KubernetesHelmApplicationController {
|
||||
async getHelmApplication() {
|
||||
try {
|
||||
this.state.dataLoading = true;
|
||||
const releases = await this.HelmService.listReleases({ selector: `name=${this.state.params.name}`, namespace: 'default' });
|
||||
const releases = await this.HelmService.listReleases(this.endpoint.Id, { selector: `name=${this.state.params.name}`, namespace: 'default' });
|
||||
if (releases.length > 0) {
|
||||
this.state.release = releases[0];
|
||||
} else {
|
||||
|
||||
@@ -5,4 +5,7 @@ import './helm.css';
|
||||
angular.module('portainer.kubernetes').component('kubernetesHelmApplicationView', {
|
||||
templateUrl: './helm.html',
|
||||
controller,
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user