Pull in all changes from tech review in EE-943

This commit is contained in:
Matt Hook
2021-08-25 05:14:57 +12:00
committed by zees-dev
parent caad31def7
commit 16933e5f9f
22 changed files with 153 additions and 722 deletions

View File

@@ -1,17 +1,15 @@
package helm
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/portainer/libhelm"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/exec/helm"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/kubernetes"
)
const (
@@ -27,14 +25,16 @@ type Handler struct {
*mux.Router
requestBouncer requestBouncer
DataStore portainer.DataStore
HelmPackageManager helm.HelmPackageManager
kubeConfigService kubernetes.KubeConfigService
HelmPackageManager libhelm.HelmPackageManager
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer requestBouncer) *Handler {
func NewHandler(bouncer requestBouncer, kubeConfigService kubernetes.KubeConfigService) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
Router: mux.NewRouter(),
requestBouncer: bouncer,
kubeConfigService: kubeConfigService,
}
// `helm install [NAME] [CHART] flags`
@@ -77,22 +77,3 @@ func (handler *Handler) GetEndpoint(r *http.Request) (*portainer.Endpoint, *http
return endpoint, nil
}
// getProxyUrl generates portainer proxy url which acts as proxy to k8s api server
func getProxyUrl(r *http.Request, endpointID portainer.EndpointID) string {
return fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpointID)
}
// extractBearerToken extracts user's portainer bearer token from request auth header
func extractBearerToken(r *http.Request) (string, error) {
token := ""
tokens := r.Header["Authorization"]
if len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
return "", httperrors.ErrUnauthorized
}
return token, nil
}

View File

@@ -1,28 +1,34 @@
package helm
import (
"errors"
"fmt"
"net/http"
"os"
"strings"
"github.com/portainer/libhelm/options"
"github.com/portainer/libhelm/release"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/exec/helm"
"github.com/portainer/portainer/api/exec/helm/release"
"github.com/portainer/portainer/api/http/security"
validation "github.com/portainer/portainer/api/kubernetes/validation"
)
const defaultHelmRepoURL = "https://charts.bitnami.com/bitnami"
type installChartPayload struct {
Namespace string `json:namespace`
Namespace string `json:"namespace"`
Name string `json:"name"`
Chart string `json:"chart"`
Values string `json:"values"`
}
var errChartNameInvalid = errors.New("invalid chart name. " +
"Chart name must consist of lower case alphanumeric characters, '-' or '.'," +
" and must start and end with an alphanumeric character",
)
func (p *installChartPayload) Validate(_ *http.Request) error {
var required []string
if p.Name == "" {
@@ -37,6 +43,11 @@ func (p *installChartPayload) Validate(_ *http.Request) error {
if len(required) > 0 {
return fmt.Errorf("required field(s) missing: %s", strings.Join(required, ", "))
}
if errs := validation.IsDNS1123Subdomain(p.Name); len(errs) > 0 {
return errChartNameInvalid
}
return nil
}
@@ -70,7 +81,7 @@ func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *htt
return httperr
}
bearerToken, err := extractBearerToken(r)
bearerToken, err := security.ExtractBearerToken(r)
if err != nil {
return &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
}
@@ -89,7 +100,7 @@ func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *htt
}
}
release, err := handler.installChart(settings.HelmRepositoryURL, endpoint, payload, getProxyUrl(r, endpoint.ID), bearerToken)
release, err := handler.installChart(settings.HelmRepositoryURL, endpoint, payload, bearerToken)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
@@ -102,12 +113,18 @@ func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *htt
return response.JSON(w, release)
}
func (handler *Handler) installChart(repo string, endpoint *portainer.Endpoint, p *installChartPayload, serverURL, bearerToken string) (*release.Release, error) {
installOpts := helm.InstallOptions{
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,
KubernetesClusterAccess: &options.KubernetesClusterAccess{
ClusterServerURL: clusterAccess.ClusterServerURL,
CertificateAuthorityFile: clusterAccess.CertificateAuthorityFile,
AuthToken: clusterAccess.AuthToken,
},
}
if p.Values != "" {
@@ -128,7 +145,7 @@ func (handler *Handler) installChart(repo string, endpoint *portainer.Endpoint,
installOpts.ValuesFile = file.Name()
}
release, err := handler.HelmPackageManager.Install(installOpts, endpoint.ID, bearerToken)
release, err := handler.HelmPackageManager.Install(installOpts)
if err != nil {
return nil, err
}

View File

@@ -3,8 +3,9 @@ package helm
import (
"net/http"
"github.com/portainer/libhelm"
"github.com/portainer/libhelm/options"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api/exec/helm"
)
// @id HelmRepoSearch
@@ -26,11 +27,11 @@ func (handler *Handler) helmRepoSearch(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve settings", Err: err}
}
searchOpts := helm.SearchRepoOptions{
searchOpts := options.SearchRepoOptions{
Repo: settings.HelmRepositoryURL,
}
result, err := handler.HelmPackageManager.SearchRepo(searchOpts)
result, err := libhelm.SearchRepo(searchOpts)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
@@ -40,7 +41,7 @@ func (handler *Handler) helmRepoSearch(w http.ResponseWriter, r *http.Request) *
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(result))
w.Write(result)
return nil
}

View File

@@ -6,9 +6,8 @@ import (
"net/http/httptest"
"testing"
"github.com/portainer/libhelm/binary/test"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/exec/helm/test"
"github.com/portainer/portainer/api/kubernetes"
"github.com/stretchr/testify/assert"
helper "github.com/portainer/portainer/api/internal/testhelpers"
@@ -22,7 +21,7 @@ func Test_helmRepoSearch(t *testing.T) {
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
}
h.DataStore = helper.NewDatastore(helper.WithSettingsService(defaultSettings))
h.HelmPackageManager = test.NewMockHelmBinaryPackageManager(kubernetes.NewKubeConfigCAService("", ""), "")
h.HelmPackageManager = test.NewMockHelmBinaryPackageManager("")
t.Run("helmRepoSearch", func(t *testing.T) {
is := assert.New(t)
@@ -33,8 +32,7 @@ func Test_helmRepoSearch(t *testing.T) {
is.Equal(rr.Code, http.StatusOK, "Status should be 200 OK")
body, err := io.ReadAll(rr.Body)
_, err := io.ReadAll(rr.Body)
is.NoError(err, "ReadAll should not return error")
is.EqualValues(string(body), test.MockDataIndex, "Unexpected search response")
})
}

View File

@@ -4,9 +4,9 @@ import (
"log"
"net/http"
"github.com/portainer/libhelm/options"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api/exec/helm"
)
// @id HelmList
@@ -43,8 +43,8 @@ func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httper
log.Printf("[DEBUG] [internal,helm] [message: command not provided, defaulting to %s]", cmd)
}
showOptions := helm.ShowOptions{
OutputFormat: helm.ShowOutputFormat(cmd),
showOptions := options.ShowOptions{
OutputFormat: options.ShowOutputFormat(cmd),
Chart: chart,
Repo: settings.HelmRepositoryURL,
}
@@ -58,7 +58,7 @@ func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httper
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(result))
w.Write(result)
return nil
}

View File

@@ -7,10 +7,9 @@ import (
"net/http/httptest"
"testing"
"github.com/portainer/libhelm/binary/test"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/exec/helm/test"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/kubernetes"
"github.com/stretchr/testify/assert"
)
@@ -25,7 +24,7 @@ func Test_helmShow(t *testing.T) {
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
}
h.DataStore = helper.NewDatastore(helper.WithSettingsService(defaultSettings))
h.HelmPackageManager = test.NewMockHelmBinaryPackageManager(kubernetes.NewKubeConfigCAService("", ""), "")
h.HelmPackageManager = test.NewMockHelmBinaryPackageManager("")
commands := map[string]string{
"values": test.MockDataValues,

View File

@@ -3,6 +3,8 @@ package kubernetes
import (
"errors"
"fmt"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
@@ -10,8 +12,6 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/security"
kcli "github.com/portainer/portainer/api/kubernetes/cli"
"net/http"
)
// @id GetKubernetesConfig

View File

@@ -206,18 +206,12 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
token = r.URL.Query().Get("token")
// Get token from the Authorization header
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized)
token, err := ExtractBearerToken(r)
if err != nil {
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", err)
return
}
var err error
tokenData, err = bouncer.jwtService.ParseAndVerifyToken(token)
if err != nil {
httperror.WriteError(w, http.StatusUnauthorized, "Invalid JWT token", err)
@@ -239,6 +233,19 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
})
}
func ExtractBearerToken(r *http.Request) (string, error) {
token := ""
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
return "", httperrors.ErrUnauthorized
}
return token, nil
}
// mwSecureHeaders provides secure headers middleware for handlers.
func mwSecureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@@ -9,11 +9,11 @@ import (
"path/filepath"
"time"
"github.com/portainer/libhelm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/exec/helm"
"github.com/portainer/portainer/api/http/handler"
"github.com/portainer/portainer/api/http/handler/auth"
"github.com/portainer/portainer/api/http/handler/backup"
@@ -86,7 +86,7 @@ type Server struct {
DockerClientFactory *docker.ClientFactory
KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer
HelmPackageManager helm.HelmPackageManager
HelmPackageManager libhelm.HelmPackageManager
Scheduler *scheduler.Scheduler
ShutdownCtx context.Context
ShutdownTrigger context.CancelFunc
@@ -171,7 +171,7 @@ func (server *Server) Start() error {
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
var endpointHelmHandler = helmhandler.NewHandler(requestBouncer)
var endpointHelmHandler = helmhandler.NewHandler(requestBouncer, server.KubeConfigService)
endpointHelmHandler.DataStore = server.DataStore
endpointHelmHandler.HelmPackageManager = server.HelmPackageManager