fix(kubernetes): create proxied kubeclient EE-4326 (#7850)

pull/7773/head
Dakota Walsh 2022-10-18 10:46:27 +13:00 committed by GitHub
parent f6d6be90e4
commit 0c995ae1c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 485 additions and 71 deletions

View File

@ -239,8 +239,8 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService,
return docker.NewClientFactory(signatureService, reverseTunnelService)
}
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *kubecli.ClientFactory {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
}
func initSnapshotService(
@ -612,7 +612,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil {

View File

@ -31,6 +31,7 @@ require (
github.com/json-iterator/go v1.1.12
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a

View File

@ -352,6 +352,8 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

@ -2,6 +2,7 @@ package kubernetes
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@ -9,7 +10,23 @@ import (
)
func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
@ -22,7 +39,7 @@ func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.R
configmaps, err := cli.GetConfigMapsAndSecrets(namespace)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to retrieve configmaps and secrets",
err,
)
}

View File

@ -3,6 +3,8 @@ package kubernetes
import (
"errors"
"net/http"
"net/url"
"strconv"
portainer "github.com/portainer/portainer/api"
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
@ -24,7 +26,6 @@ type Handler struct {
*mux.Router
authorizationService *authorization.Service
DataStore dataservices.DataStore
KubernetesClient portainer.KubeClient
KubernetesClientFactory *cli.ClientFactory
JwtService dataservices.JWTService
kubeClusterAccessService kubernetes.KubeClusterAccessService
@ -39,7 +40,6 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
JwtService: jwtService,
kubeClusterAccessService: kubeClusterAccessService,
KubernetesClientFactory: kubernetesClientFactory,
KubernetesClient: kubernetesClient,
}
kubeRouter := h.PathPrefix("/kubernetes").Subrouter()
@ -85,13 +85,19 @@ func kubeOnlyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
endpoint, err := middlewares.FetchEndpoint(request)
if err != nil {
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err)
httperror.InternalServerError(
"Unable to find an environment on request context",
err,
)
return
}
if !endpointutils.IsKubernetesEndpoint(endpoint) {
errMessage := "environment is not a Kubernetes environment"
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
httperror.BadRequest(
errMessage,
errors.New(errMessage),
)
return
}
@ -109,6 +115,7 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler {
"Invalid environment identifier route variable",
err,
)
return
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
@ -119,6 +126,7 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler {
"Unable to find an environment with the specified identifier inside the database",
err,
)
return
} else if err != nil {
httperror.WriteError(
w,
@ -126,23 +134,101 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler {
"Unable to find an environment with the specified identifier inside the database",
err,
)
return
}
if handler.KubernetesClientFactory == nil {
next.ServeHTTP(w, r)
return
}
kubeCli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
// Generate a proxied kubeconfig, then create a kubeclient using it.
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteError(
w,
http.StatusForbidden,
"Permission denied to access environment",
err,
)
return
}
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
if err != nil {
httperror.WriteError(
w,
http.StatusInternalServerError,
"Unable to create Kubernetes client",
"Unable to create JWT token",
err,
)
return
}
handler.KubernetesClient = kubeCli
singleEndpointList := []portainer.Endpoint{
*endpoint,
}
config, handlerErr := handler.buildConfig(
r,
tokenData,
bearerToken,
singleEndpointList,
)
if err != nil {
httperror.WriteError(
w,
http.StatusInternalServerError,
"Unable to build endpoint kubeconfig",
handlerErr.Err,
)
return
}
if len(config.Clusters) == 0 {
httperror.WriteError(
w,
http.StatusInternalServerError,
"Unable build cluster kubeconfig",
errors.New("Unable build cluster kubeconfig"),
)
return
}
// Manually setting the localhost to route
// the request to proxy server
serverURL, err := url.Parse(config.Clusters[0].Cluster.Server)
if err != nil {
httperror.WriteError(
w,
http.StatusInternalServerError,
"Unable parse cluster's kubeconfig server URL",
nil,
)
return
}
serverURL.Scheme = "https"
serverURL.Host = "localhost" + handler.KubernetesClientFactory.AddrHTTPS
config.Clusters[0].Cluster.Server = serverURL.String()
yaml, err := cli.GenerateYAML(config)
if err != nil {
httperror.WriteError(
w,
http.StatusInternalServerError,
"Unable to generate yaml from endpoint kubeconfig",
err,
)
return
}
kubeCli, err := handler.KubernetesClientFactory.CreateKubeClientFromKubeConfig(endpoint.Name, []byte(yaml))
if err != nil {
httperror.WriteError(
w,
http.StatusInternalServerError,
"Failed to create client from kubeconfig",
err,
)
return
}
handler.KubernetesClientFactory.SetProxyKubeClient(strconv.Itoa(int(endpoint.ID)), r.Header.Get("Authorization"), kubeCli)
next.ServeHTTP(w, r)
})
}

View File

@ -2,6 +2,7 @@ package kubernetes
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@ -49,7 +50,13 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
)
}
controllers := cli.GetIngressControllers()
controllers, err := cli.GetIngressControllers()
if err != nil {
return httperror.InternalServerError(
"Failed to fetch ingressclasses",
err,
)
}
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
var updatedClasses []portainer.KubernetesIngressClassConfig
for i := range controllers {
@ -129,8 +136,23 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon
)
}
cli := handler.KubernetesClient
currentControllers := cli.GetIngressControllers()
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
currentControllers, err := cli.GetIngressControllers()
if err != nil {
return httperror.InternalServerError(
"Failed to fetch ingressclasses",
err,
)
}
kubernetesConfig := endpoint.Kubernetes.Configuration
existingClasses := kubernetesConfig.IngressClasses
ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace
@ -229,7 +251,13 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter
}
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
controllers := cli.GetIngressControllers()
controllers, err := cli.GetIngressControllers()
if err != nil {
return httperror.InternalServerError(
"Unable to get ingress controllers",
err,
)
}
var updatedClasses []portainer.KubernetesIngressClassConfig
for i := range controllers {
controllers[i].Availability = true
@ -401,11 +429,28 @@ func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Re
)
}
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
ingresses, err := cli.GetIngresses(namespace)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to retrieve ingresses",
err,
)
}
@ -431,11 +476,28 @@ func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.R
)
}
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
err = cli.CreateIngress(namespace, payload)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to retrieve the ingress",
err,
)
}
@ -443,10 +505,26 @@ func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.R
}
func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
var payload models.K8sIngressDeleteRequests
err := request.DecodeAndValidateJSONPayload(r, &payload)
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
@ -454,7 +532,7 @@ func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http
err = cli.DeleteIngresses(payload)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to delete ingresses",
err,
)
}
@ -479,11 +557,28 @@ func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.R
)
}
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
err = cli.UpdateIngress(namespace, payload)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to update the ingress",
err,
)
}

View File

@ -2,6 +2,7 @@ package kubernetes
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@ -10,12 +11,28 @@ import (
)
func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
namespaces, err := cli.GetNamespaces()
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to retrieve namespaces",
err,
)
}
@ -24,10 +41,26 @@ func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.R
}
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
var payload models.K8sNamespaceDetails
err := request.DecodeAndValidateJSONPayload(r, &payload)
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest(
"Invalid request payload",
@ -38,7 +71,7 @@ func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http
err = cli.CreateNamespace(payload)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to create namespace",
err,
)
}
@ -46,7 +79,23 @@ func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http
}
func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
@ -59,7 +108,7 @@ func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *htt
err = cli.DeleteNamespace(namespace)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to delete namespace",
err,
)
}
@ -68,17 +117,33 @@ func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *htt
}
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
var payload models.K8sNamespaceDetails
err := request.DecodeAndValidateJSONPayload(r, &payload)
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
err = cli.UpdateNamespace(payload)
if err != nil {
return httperror.InternalServerError("Unable to retrieve nodes limits", err)
return httperror.InternalServerError("Unable to update namespace", err)
}
return nil
}

View File

@ -2,6 +2,7 @@ package kubernetes
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@ -18,7 +19,24 @@ func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Req
)
}
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
services, err := cli.GetServices(namespace)
if err != nil {
return httperror.InternalServerError(
@ -48,11 +66,28 @@ func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.R
)
}
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
err = cli.CreateService(namespace, payload)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to create sercice",
err,
)
}
@ -60,10 +95,26 @@ func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.R
}
func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
var payload models.K8sServiceDeleteRequests
err := request.DecodeAndValidateJSONPayload(r, &payload)
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest(
"Invalid request payload",
@ -74,7 +125,7 @@ func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.
err = cli.DeleteServices(payload)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to delete service",
err,
)
}
@ -99,11 +150,27 @@ func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.R
)
}
cli := handler.KubernetesClient
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
}
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
)
if !ok {
return httperror.InternalServerError(
"Failed to lookup KubeClient",
nil,
)
}
err = cli.UpdateService(namespace, payload)
if err != nil {
return httperror.InternalServerError(
"Unable to retrieve nodes limits",
"Unable to update service",
err,
)
}

View File

@ -5,8 +5,10 @@ import (
"net/http"
"strconv"
"sync"
"time"
cmap "github.com/orcaman/concurrent-map"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
@ -23,6 +25,8 @@ type (
signatureService portainer.DigitalSignatureService
instanceID string
endpointClients cmap.ConcurrentMap
endpointProxyClients *cache.Cache
AddrHTTPS string
}
// KubeClient represent a service used to execute Kubernetes operations
@ -34,14 +38,24 @@ type (
)
// NewClientFactory returns a new instance of a ClientFactory
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *ClientFactory {
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*ClientFactory, error) {
if userSessionTimeout == "" {
userSessionTimeout = portainer.DefaultUserSessionTimeout
}
timeout, err := time.ParseDuration(userSessionTimeout)
if err != nil {
return nil, err
}
return &ClientFactory{
dataStore: dataStore,
signatureService: signatureService,
reverseTunnelService: reverseTunnelService,
instanceID: instanceID,
endpointClients: cmap.New(),
}
endpointProxyClients: cache.New(timeout, timeout),
AddrHTTPS: addrHTTPS,
}, nil
}
func (factory *ClientFactory) GetInstanceID() (instanceID string) {
@ -59,7 +73,7 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (porta
key := strconv.Itoa(int(endpoint.ID))
client, ok := factory.endpointClients.Get(key)
if !ok {
client, err := factory.createKubeClient(endpoint)
client, err := factory.createCachedAdminKubeClient(endpoint)
if err != nil {
return nil, err
}
@ -71,7 +85,49 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (porta
return client.(portainer.KubeClient), nil
}
func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {
// GetProxyKubeClient retrieves a KubeClient from the cache. You should be
// calling SetProxyKubeClient before first. It is normally, called the
// kubernetes middleware.
func (factory *ClientFactory) GetProxyKubeClient(endpointID, token string) (portainer.KubeClient, bool) {
client, ok := factory.endpointProxyClients.Get(endpointID + "." + token)
if !ok {
return nil, false
}
return client.(portainer.KubeClient), true
}
// SetProxyKubeClient stores a kubeclient in the cache.
func (factory *ClientFactory) SetProxyKubeClient(endpointID, token string, cli portainer.KubeClient) {
factory.endpointProxyClients.Set(endpointID+"."+token, cli, 0)
}
// CreateKubeClientFromKubeConfig creates a KubeClient from a clusterID, and
// Kubernetes config.
func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, kubeConfig []byte) (portainer.KubeClient, error) {
config, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig))
if err != nil {
return nil, err
}
cliConfig, err := config.ClientConfig()
if err != nil {
return nil, err
}
cli, err := kubernetes.NewForConfig(cliConfig)
if err != nil {
return nil, err
}
kubecli := &KubeClient{
cli: cli,
instanceID: factory.instanceID,
lock: &sync.Mutex{},
}
return kubecli, nil
}
func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {
cli, err := factory.CreateClient(endpoint)
if err != nil {
return nil, err

View File

@ -5,11 +5,12 @@ import (
"strings"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers {
func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) {
var controllers []models.K8sIngressController
// We know that each existing class points to a controller so we can start
@ -17,19 +18,22 @@ func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers {
classClient := kcl.cli.NetworkingV1().IngressClasses()
classList, err := classClient.List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil
return nil, err
}
// We want to know which of these controllers is in use.
var ingresses []models.K8sIngressInfo
namespaces, err := kcl.GetNamespaces()
if err != nil {
return nil
return nil, err
}
for namespace := range namespaces {
t, err := kcl.GetIngresses(namespace)
if err != nil {
return nil
// User might not be able to list ingresses in system/not allowed
// namespaces.
log.Debug().Err(err).Msg("failed to list ingresses for the current user, skipped sending ingress")
continue
}
ingresses = append(ingresses, t...)
}
@ -58,7 +62,7 @@ func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers {
}
controllers = append(controllers, controller)
}
return controllers
return controllers, nil
}
// GetIngresses gets all the ingresses for a given namespace in a k8s endpoint.

View File

@ -85,7 +85,7 @@ func Test_GenerateYAML(t *testing.T) {
t.Errorf("generateYamlConfig failed; err=%s", err)
}
if compareYAMLStrings(yaml, ryt.wantYAML) != 0 {
if compareYAMLStrings(string(yaml), ryt.wantYAML) != 0 {
t.Errorf("generateYamlConfig failed;\ngot=\n%s\nwant=\n%s", yaml, ryt.wantYAML)
}
})

View File

@ -25,6 +25,11 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
Resources: []string{"namespaces", "pods", "nodes"},
APIGroups: []string{"metrics.k8s.io"},
},
{
Verbs: []string{"list"},
Resources: []string{"ingressclasses"},
APIGroups: []string{"networking.k8s.io"},
},
}
}

View File

@ -106,7 +106,7 @@ func (service *kubeClusterAccessService) GetData(hostURL string, endpointID port
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
}
log.Info().
log.Debug().
Str("host_URL", hostURL).
Str("HTTPS_bind_address", service.httpsBindAddr).
Str("base_URL", baseURL).

View File

@ -1357,7 +1357,7 @@ type (
GetNamespaces() (map[string]K8sNamespaceInfo, error)
DeleteNamespace(namespace string) error
GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error)
GetIngressControllers() models.K8sIngressControllers
GetIngressControllers() (models.K8sIngressControllers, error)
CreateIngress(namespace string, info models.K8sIngressInfo) error
UpdateIngress(namespace string, info models.K8sIngressInfo) error
GetIngresses(namespace string) ([]models.K8sIngressInfo, error)

View File

@ -3,7 +3,7 @@ import { useRouter } from '@uirouter/react';
import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
import { useNamespaces } from '@/react/kubernetes/namespaces/queries';
import { Authorized } from '@/portainer/hooks/useUser';
import { useAuthorizations, Authorized } from '@/portainer/hooks/useUser';
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
import { Datatable } from '@@/datatables';
@ -55,6 +55,7 @@ export function IngressDataTable() {
}}
getRowId={(row) => row.Name + row.Type + row.Namespace}
renderTableActions={tableActions}
disableSelect={useCheckboxes()}
/>
);
@ -80,7 +81,7 @@ export function IngressDataTable() {
</Button>
</Authorized>
<Authorized authorizations="K8sIngressesAdd">
<Authorized authorizations="K8sIngressesW">
<Link to="kubernetes.ingresses.create" className="space-left">
<Button
icon={Plus}
@ -91,7 +92,7 @@ export function IngressDataTable() {
</Button>
</Link>
</Authorized>
<Authorized authorizations="K8sApplicationsW">
<Authorized authorizations="K8sIngressesW">
<Link to="kubernetes.deploy" className="space-left">
<Button icon={Plus} className="btn-wrapper">
Create from manifest
@ -102,6 +103,10 @@ export function IngressDataTable() {
);
}
function useCheckboxes() {
return !useAuthorizations(['K8sIngressesW']);
}
async function handleRemoveClick(ingresses: SelectedIngress[]) {
const confirmed = await confirmDeletionAsync(
'Are you sure you want to delete the selected ingresses?'

View File

@ -1,5 +1,7 @@
import { CellProps, Column } from 'react-table';
import { Authorized } from '@/portainer/hooks/useUser';
import { Link } from '@@/Link';
import { Ingress } from '../../types';
@ -8,17 +10,22 @@ export const name: Column<Ingress> = {
Header: 'Name',
accessor: 'Name',
Cell: ({ row }: CellProps<Ingress>) => (
<Link
to="kubernetes.ingresses.edit"
params={{
uid: row.original.UID,
namespace: row.original.Namespace,
name: row.original.Name,
}}
title={row.original.Name}
<Authorized
authorizations="K8sIngressesW"
childrenUnauthorized={row.original.Name}
>
{row.original.Name}
</Link>
<Link
to="kubernetes.ingresses.edit"
params={{
uid: row.original.UID,
namespace: row.original.Namespace,
name: row.original.Name,
}}
title={row.original.Name}
>
{row.original.Name}
</Link>
</Authorized>
),
id: 'name',
disableFilters: true,

View File

@ -11,7 +11,9 @@
</tr>
<tr ng-repeat="ingress in $ctrl.applicationIngress">
<td
><a ui-sref="kubernetes.ingresses.edit({ name: ingress.IngressName, namespace: $ctrl.application.ResourcePool })">{{ ingress.IngressName }}</a></td
><a authorization="K8sIngressesW" ui-sref="kubernetes.ingresses.edit({ name: ingress.IngressName, namespace: $ctrl.application.ResourcePool })">{{
ingress.IngressName
}}</a></td
>
<td>{{ ingress.ServiceName }}</td>
<td>{{ ingress.Host }}</td>

View File

@ -123,6 +123,7 @@ interface AuthorizedProps {
authorizations: string | string[];
environmentId?: EnvironmentId;
adminOnlyCE?: boolean;
childrenUnauthorized?: ReactNode;
}
export function Authorized({
@ -130,6 +131,7 @@ export function Authorized({
environmentId,
adminOnlyCE = false,
children,
childrenUnauthorized = null,
}: PropsWithChildren<AuthorizedProps>) {
const isAllowed = useAuthorizations(
authorizations,
@ -137,7 +139,7 @@ export function Authorized({
adminOnlyCE
);
return isAllowed ? <>{children}</> : null;
return isAllowed ? <>{children}</> : <>{childrenUnauthorized}</>;
}
interface UserProviderProps {