fix(kube): Use KubeClusterAccessService for Helm operations [EE-2500] (#6559)

pull/6665/head
Marcelo Rydel 2022-03-21 09:51:29 -03:00 committed by GitHub
parent cf7746082b
commit c486130a9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 105 additions and 103 deletions

View File

@ -599,7 +599,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
kubeConfigService := kubernetes.NewKubeConfigCAService(*flags.AddrHTTPS, sslSettings.CertPath)
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
@ -706,7 +706,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
OpenAMTService: openAMTService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeConfigService: kubeConfigService,
KubeClusterAccessService: kubeClusterAccessService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSLService: sslService,
@ -716,7 +716,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
BaseURL: *flags.BaseURL,
}
}

View File

@ -2,6 +2,7 @@ package helm
import (
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/portainer/libhelm"
@ -14,10 +15,6 @@ import (
"github.com/portainer/portainer/api/kubernetes"
)
const (
handlerActivityContext = "Kubernetes"
)
type requestBouncer interface {
AuthenticatedAccess(h http.Handler) http.Handler
}
@ -25,24 +22,24 @@ type requestBouncer interface {
// Handler is the HTTP handler used to handle environment(endpoint) group operations.
type Handler struct {
*mux.Router
requestBouncer requestBouncer
dataStore dataservices.DataStore
jwtService dataservices.JWTService
kubeConfigService kubernetes.KubeConfigService
kubernetesDeployer portainer.KubernetesDeployer
helmPackageManager libhelm.HelmPackageManager
requestBouncer requestBouncer
dataStore dataservices.DataStore
jwtService dataservices.JWTService
kubeClusterAccessService kubernetes.KubeClusterAccessService
kubernetesDeployer portainer.KubernetesDeployer
helmPackageManager libhelm.HelmPackageManager
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer requestBouncer, dataStore dataservices.DataStore, jwtService dataservices.JWTService, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
func NewHandler(bouncer requestBouncer, dataStore dataservices.DataStore, jwtService dataservices.JWTService, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeClusterAccessService kubernetes.KubeClusterAccessService) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
dataStore: dataStore,
jwtService: jwtService,
kubernetesDeployer: kubernetesDeployer,
helmPackageManager: helmPackageManager,
kubeConfigService: kubeConfigService,
Router: mux.NewRouter(),
requestBouncer: bouncer,
dataStore: dataStore,
jwtService: jwtService,
kubernetesDeployer: kubernetesDeployer,
helmPackageManager: helmPackageManager,
kubeClusterAccessService: kubeClusterAccessService,
}
h.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
@ -104,10 +101,20 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
return nil, &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
}
kubeConfigInternal := handler.kubeConfigService.GetKubeConfigInternal(endpoint.ID, bearerToken)
sslSettings, err := handler.dataStore.SSLSettings().Settings()
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
hostURL := "localhost"
if !sslSettings.SelfSigned {
hostURL = strings.Split(r.Host, ":")[0]
}
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
return &options.KubernetesClusterAccess{
ClusterServerURL: kubeConfigInternal.ClusterServerURL,
CertificateAuthorityFile: kubeConfigInternal.CertificateAuthorityFile,
AuthToken: kubeConfigInternal.AuthToken,
AuthToken: bearerToken,
}, nil
}

View File

@ -36,8 +36,8 @@ func Test_helmDelete(t *testing.T) {
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeConfigService)
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)
is.NotNil(h, "Handler should not fail")

View File

@ -39,8 +39,8 @@ func Test_helmInstall(t *testing.T) {
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeConfigService)
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)
is.NotNil(h, "Handler should not fail")

View File

@ -38,8 +38,8 @@ func Test_helmList(t *testing.T) {
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeConfigService)
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)
// Install a single chart. We expect to get these values back
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}

View File

@ -2,6 +2,7 @@ package kubernetes
import (
"errors"
"github.com/portainer/portainer/api/kubernetes"
"net/http"
"github.com/gorilla/mux"
@ -17,21 +18,22 @@ import (
// Handler is the HTTP handler which will natively deal with to external environments(endpoints).
type Handler struct {
*mux.Router
dataStore dataservices.DataStore
kubernetesClientFactory *cli.ClientFactory
authorizationService *authorization.Service
JwtService dataservices.JWTService
BaseURL string
authorizationService *authorization.Service
dataStore dataservices.DataStore
jwtService dataservices.JWTService
kubernetesClientFactory *cli.ClientFactory
kubeClusterAccessService kubernetes.KubeClusterAccessService
}
// NewHandler creates a handler to process pre-proxied requests to external APIs.
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, kubernetesClientFactory *cli.ClientFactory, baseURL string) *Handler {
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, jwtService dataservices.JWTService, kubeClusterAccessService kubernetes.KubeClusterAccessService, kubernetesClientFactory *cli.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
dataStore: dataStore,
kubernetesClientFactory: kubernetesClientFactory,
authorizationService: authorizationService,
BaseURL: baseURL,
Router: mux.NewRouter(),
authorizationService: authorizationService,
dataStore: dataStore,
jwtService: jwtService,
kubeClusterAccessService: kubeClusterAccessService,
kubernetesClientFactory: kubernetesClientFactory,
}
kubeRouter := h.PathPrefix("/kubernetes").Subrouter()

View File

@ -39,7 +39,7 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
}
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
bearerToken, err := handler.jwtService.GenerateTokenForKubeconfig(tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
}
@ -126,7 +126,7 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
instanceID := handler.kubernetesClientFactory.GetInstanceID()
serviceAccountName := kcli.UserServiceAccountName(int(tokenData.ID), instanceID)
configClusters[idx] = buildCluster(r, handler.BaseURL, endpoint)
configClusters[idx] = handler.buildCluster(r, endpoint)
configContexts[idx] = buildContext(serviceAccountName, endpoint)
if !authInfosSet[serviceAccountName] {
configAuthInfos = append(configAuthInfos, buildAuthInfo(serviceAccountName, bearerToken))
@ -144,15 +144,13 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
}, nil
}
func buildCluster(r *http.Request, baseURL string, endpoint portainer.Endpoint) clientV1.NamedCluster {
if baseURL != "/" {
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
}
proxyURL := fmt.Sprintf("https://%s%sapi/endpoints/%d/kubernetes", r.Host, baseURL, endpoint.ID)
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
hostURL := strings.Split(r.Host, ":")[0]
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
return clientV1.NamedCluster{
Name: buildClusterName(endpoint.Name),
Cluster: clientV1.Cluster{
Server: proxyURL,
Server: kubeConfigInternal.ClusterServerURL,
InsecureSkipTLSVerify: true,
},
}

View File

@ -87,7 +87,7 @@ type Server struct {
SwarmStackManager portainer.SwarmStackManager
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
KubeConfigService k8s.KubeConfigService
KubeClusterAccessService k8s.KubeClusterAccessService
Handler *handler.Handler
SSLService *ssl.Service
DockerClientFactory *docker.ClientFactory
@ -98,7 +98,6 @@ type Server struct {
ShutdownCtx context.Context
ShutdownTrigger context.CancelFunc
StackDeployer stackdeployer.StackDeployer
BaseURL string
}
// Start starts the HTTP server
@ -175,12 +174,11 @@ func (server *Server) Start() error {
endpointProxyHandler.ProxyManager = server.ProxyManager
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.KubernetesClientFactory, server.BaseURL)
kubernetesHandler.JwtService = server.JWTService
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory)
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeConfigService)
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService)
var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager)

View File

@ -7,26 +7,27 @@ import (
"fmt"
"io/ioutil"
"log"
"strings"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
// KubeConfigService represents a service that is responsible for handling kubeconfig operations
type KubeConfigService interface {
// KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data
type KubeClusterAccessService interface {
IsSecure() bool
GetKubeConfigInternal(endpointId portainer.EndpointID, authToken string) kubernetesClusterAccess
GetData(hostURL string, endpointId portainer.EndpointID) kubernetesClusterAccessData
}
// KubernetesClusterAccess represents core details which can be used to generate KubeConfig file/data
type kubernetesClusterAccess struct {
type kubernetesClusterAccessData struct {
ClusterServerURL string `example:"https://mycompany.k8s.com"`
CertificateAuthorityFile string `example:"/data/tls/localhost.crt"`
CertificateAuthorityData string `example:"MIIC5TCCAc2gAwIBAgIJAJ+...+xuhOaFXwQ=="`
AuthToken string `example:"ey..."`
}
type kubeConfigCAService struct {
type kubeClusterAccessService struct {
baseURL string
httpsBindAddr string
certificateAuthorityFile string
certificateAuthorityData string
@ -39,14 +40,15 @@ var (
errTLSCertValidation = errors.New("failed to parse tls certificate")
)
// NewKubeConfigCAService encapsulates generation of core KubeConfig data
func NewKubeConfigCAService(httpsBindAddr string, tlsCertPath string) KubeConfigService {
// NewKubeClusterAccessService creates a new instance of a KubeClusterAccessService
func NewKubeClusterAccessService(baseURL, httpsBindAddr, tlsCertPath string) KubeClusterAccessService {
certificateAuthorityData, err := getCertificateAuthorityData(tlsCertPath)
if err != nil {
log.Printf("[DEBUG] [internal,kubeconfig] [message: %s, generated KubeConfig will be insecure]", err.Error())
}
return &kubeConfigCAService{
return &kubeClusterAccessService{
baseURL: baseURL,
httpsBindAddr: httpsBindAddr,
certificateAuthorityFile: tlsCertPath,
certificateAuthorityData: certificateAuthorityData,
@ -82,23 +84,27 @@ func getCertificateAuthorityData(tlsCertPath string) (string, error) {
// this is based on the fact that we can successfully extract `certificateAuthorityData` from
// certificate file at `tlsCertPath`. If we can successfully extract `certificateAuthorityData`,
// then this will be used as `certificate-authority-data` attribute in a generated KubeConfig.
func (kccas *kubeConfigCAService) IsSecure() bool {
return kccas.certificateAuthorityData != ""
func (service *kubeClusterAccessService) IsSecure() bool {
return service.certificateAuthorityData != ""
}
// GetKubeConfigInternal returns K8s cluster access details for the specified environment(endpoint).
// On startup, portainer generates a certificate against localhost at specified `httpsBindAddr` port, hence
// the kubeconfig generated should only be utilised by internal portainer binaries as the `ClusterServerURL`
// points to the internally accessible `https` based `localhost` address.
// GetData returns K8s cluster access details for the specified environment(endpoint).
// The struct can be used to:
// - generate a kubeconfig file
// - pass down params to binaries
func (kccas *kubeConfigCAService) GetKubeConfigInternal(endpointId portainer.EndpointID, authToken string) kubernetesClusterAccess {
clusterServerUrl := fmt.Sprintf("https://localhost%s/api/endpoints/%s/kubernetes", kccas.httpsBindAddr, fmt.Sprint(endpointId))
return kubernetesClusterAccess{
ClusterServerURL: clusterServerUrl,
CertificateAuthorityFile: kccas.certificateAuthorityFile,
CertificateAuthorityData: kccas.certificateAuthorityData,
AuthToken: authToken,
func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData {
baseURL := service.baseURL
if baseURL != "/" {
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
}
clusterURL := hostURL + service.httpsBindAddr + baseURL
clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID)
return kubernetesClusterAccessData{
ClusterServerURL: clusterServerURL,
CertificateAuthorityFile: service.certificateAuthorityFile,
CertificateAuthorityData: service.certificateAuthorityData,
}
}

View File

@ -78,11 +78,11 @@ func Test_getCertificateAuthorityData(t *testing.T) {
})
}
func TestKubeConfigService_IsSecure(t *testing.T) {
func TestKubeClusterAccessService_IsSecure(t *testing.T) {
is := assert.New(t)
t.Run("IsSecure should be false", func(t *testing.T) {
kcs := NewKubeConfigCAService("", "")
kcs := NewKubeClusterAccessService("", "", "")
is.False(kcs.IsSecure(), "should be false if TLS cert not provided")
})
@ -90,39 +90,32 @@ func TestKubeConfigService_IsSecure(t *testing.T) {
filePath, teardown := createTempFile("valid-cert.crt", certData)
defer teardown()
kcs := NewKubeConfigCAService("", filePath)
kcs := NewKubeClusterAccessService("", "", filePath)
is.True(kcs.IsSecure(), "should be true if valid TLS cert (path and content) provided")
})
}
func TestKubeConfigService_GetKubeConfigInternal(t *testing.T) {
func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) {
is := assert.New(t)
t.Run("GetKubeConfigInternal returns localhost address", func(t *testing.T) {
kcs := NewKubeConfigCAService("", "")
clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token")
is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "https://localhost"), "should contain localhost address")
t.Run("GetData contains host address", func(t *testing.T) {
kcs := NewKubeClusterAccessService("/", "", "")
clusterAccessDetails := kcs.GetData("mysite.com", 1)
is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "https://mysite.com"), "should contain host address")
})
t.Run("GetKubeConfigInternal contains https bind address port", func(t *testing.T) {
kcs := NewKubeConfigCAService(":1010", "")
clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token")
is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, ":1010"), "should contain bind address port")
})
t.Run("GetKubeConfigInternal contains environment proxy url", func(t *testing.T) {
kcs := NewKubeConfigCAService("", "")
clusterAccessDetails := kcs.GetKubeConfigInternal(100, "some-token")
t.Run("GetData contains environment proxy url", func(t *testing.T) {
kcs := NewKubeClusterAccessService("/", "", "")
clusterAccessDetails := kcs.GetData("mysite.com", 100)
is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "api/endpoints/100/kubernetes"), "should contain environment proxy url")
})
t.Run("GetKubeConfigInternal returns insecure cluster access config", func(t *testing.T) {
kcs := NewKubeConfigCAService("", "")
clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token")
t.Run("GetData returns insecure cluster access config", func(t *testing.T) {
kcs := NewKubeClusterAccessService("/", ":9443", "")
clusterAccessDetails := kcs.GetData("mysite.com", 1)
wantClusterAccessDetails := kubernetesClusterAccess{
ClusterServerURL: "https://localhost/api/endpoints/1/kubernetes",
AuthToken: "some-token",
wantClusterAccessDetails := kubernetesClusterAccessData{
ClusterServerURL: "https://mysite.com:9443/api/endpoints/1/kubernetes",
CertificateAuthorityFile: "",
CertificateAuthorityData: "",
}
@ -130,16 +123,15 @@ func TestKubeConfigService_GetKubeConfigInternal(t *testing.T) {
is.Equal(clusterAccessDetails, wantClusterAccessDetails)
})
t.Run("GetKubeConfigInternal returns secure cluster access config", func(t *testing.T) {
t.Run("GetData returns secure cluster access config", func(t *testing.T) {
filePath, teardown := createTempFile("valid-cert.crt", certData)
defer teardown()
kcs := NewKubeConfigCAService("", filePath)
clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token")
kcs := NewKubeClusterAccessService("/", "", filePath)
clusterAccessDetails := kcs.GetData("localhost", 1)
wantClusterAccessDetails := kubernetesClusterAccess{
wantClusterAccessDetails := kubernetesClusterAccessData{
ClusterServerURL: "https://localhost/api/endpoints/1/kubernetes",
AuthToken: "some-token",
CertificateAuthorityFile: filePath,
CertificateAuthorityData: certDataString,
}