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

View File

@ -31,6 +31,7 @@ require (
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 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/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021 github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a 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/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 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= 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/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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

@ -2,6 +2,7 @@ package kubernetes
import ( import (
"net/http" "net/http"
"strconv"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
@ -9,7 +10,23 @@ import (
) )
func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { 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") namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil { if err != nil {
@ -22,7 +39,7 @@ func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.R
configmaps, err := cli.GetConfigMapsAndSecrets(namespace) configmaps, err := cli.GetConfigMapsAndSecrets(namespace)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to retrieve configmaps and secrets",
err, err,
) )
} }

View File

@ -3,6 +3,8 @@ package kubernetes
import ( import (
"errors" "errors"
"net/http" "net/http"
"net/url"
"strconv"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors" portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
@ -24,7 +26,6 @@ type Handler struct {
*mux.Router *mux.Router
authorizationService *authorization.Service authorizationService *authorization.Service
DataStore dataservices.DataStore DataStore dataservices.DataStore
KubernetesClient portainer.KubeClient
KubernetesClientFactory *cli.ClientFactory KubernetesClientFactory *cli.ClientFactory
JwtService dataservices.JWTService JwtService dataservices.JWTService
kubeClusterAccessService kubernetes.KubeClusterAccessService kubeClusterAccessService kubernetes.KubeClusterAccessService
@ -39,7 +40,6 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
JwtService: jwtService, JwtService: jwtService,
kubeClusterAccessService: kubeClusterAccessService, kubeClusterAccessService: kubeClusterAccessService,
KubernetesClientFactory: kubernetesClientFactory, KubernetesClientFactory: kubernetesClientFactory,
KubernetesClient: kubernetesClient,
} }
kubeRouter := h.PathPrefix("/kubernetes").Subrouter() 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) { return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
endpoint, err := middlewares.FetchEndpoint(request) endpoint, err := middlewares.FetchEndpoint(request)
if err != nil { 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 return
} }
if !endpointutils.IsKubernetesEndpoint(endpoint) { if !endpointutils.IsKubernetesEndpoint(endpoint) {
errMessage := "environment is not a Kubernetes environment" errMessage := "environment is not a Kubernetes environment"
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage)) httperror.BadRequest(
errMessage,
errors.New(errMessage),
)
return return
} }
@ -109,6 +115,7 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler {
"Invalid environment identifier route variable", "Invalid environment identifier route variable",
err, err,
) )
return
} }
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) 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", "Unable to find an environment with the specified identifier inside the database",
err, err,
) )
return
} else if err != nil { } else if err != nil {
httperror.WriteError( httperror.WriteError(
w, 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", "Unable to find an environment with the specified identifier inside the database",
err, err,
) )
return
} }
if handler.KubernetesClientFactory == nil { if handler.KubernetesClientFactory == nil {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return 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 { if err != nil {
httperror.WriteError( httperror.WriteError(
w, w,
http.StatusInternalServerError, http.StatusInternalServerError,
"Unable to create Kubernetes client", "Unable to create JWT token",
err, 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) next.ServeHTTP(w, r)
}) })
} }

View File

@ -2,6 +2,7 @@ package kubernetes
import ( import (
"net/http" "net/http"
"strconv"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "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 existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
var updatedClasses []portainer.KubernetesIngressClassConfig var updatedClasses []portainer.KubernetesIngressClassConfig
for i := range controllers { for i := range controllers {
@ -129,8 +136,23 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon
) )
} }
cli := handler.KubernetesClient cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
currentControllers := cli.GetIngressControllers() 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 kubernetesConfig := endpoint.Kubernetes.Configuration
existingClasses := kubernetesConfig.IngressClasses existingClasses := kubernetesConfig.IngressClasses
ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace
@ -229,7 +251,13 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter
} }
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses 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 var updatedClasses []portainer.KubernetesIngressClassConfig
for i := range controllers { for i := range controllers {
controllers[i].Availability = true 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) ingresses, err := cli.GetIngresses(namespace)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to retrieve ingresses",
err, 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) err = cli.CreateIngress(namespace, payload)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to retrieve the ingress",
err, 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 { 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 var payload models.K8sIngressDeleteRequests
err := request.DecodeAndValidateJSONPayload(r, &payload) err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil { if err != nil {
return httperror.BadRequest("Invalid request payload", err) return httperror.BadRequest("Invalid request payload", err)
} }
@ -454,7 +532,7 @@ func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http
err = cli.DeleteIngresses(payload) err = cli.DeleteIngresses(payload)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to delete ingresses",
err, 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) err = cli.UpdateIngress(namespace, payload)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to update the ingress",
err, err,
) )
} }

View File

@ -2,6 +2,7 @@ package kubernetes
import ( import (
"net/http" "net/http"
"strconv"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
@ -10,12 +11,28 @@ import (
) )
func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { 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() namespaces, err := cli.GetNamespaces()
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to retrieve namespaces",
err, 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 { 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 var payload models.K8sNamespaceDetails
err := request.DecodeAndValidateJSONPayload(r, &payload) err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil { if err != nil {
return httperror.BadRequest( return httperror.BadRequest(
"Invalid request payload", "Invalid request payload",
@ -38,7 +71,7 @@ func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http
err = cli.CreateNamespace(payload) err = cli.CreateNamespace(payload)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to create namespace",
err, 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 { 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") namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil { if err != nil {
@ -59,7 +108,7 @@ func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *htt
err = cli.DeleteNamespace(namespace) err = cli.DeleteNamespace(namespace)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to delete namespace",
err, 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 { 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 var payload models.K8sNamespaceDetails
err := request.DecodeAndValidateJSONPayload(r, &payload) err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil { if err != nil {
return httperror.BadRequest("Invalid request payload", err) return httperror.BadRequest("Invalid request payload", err)
} }
err = cli.UpdateNamespace(payload) err = cli.UpdateNamespace(payload)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to retrieve nodes limits", err) return httperror.InternalServerError("Unable to update namespace", err)
} }
return nil return nil
} }

View File

@ -2,6 +2,7 @@ package kubernetes
import ( import (
"net/http" "net/http"
"strconv"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "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) services, err := cli.GetServices(namespace)
if err != nil { if err != nil {
return httperror.InternalServerError( 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) err = cli.CreateService(namespace, payload)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to create sercice",
err, 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 { 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 var payload models.K8sServiceDeleteRequests
err := request.DecodeAndValidateJSONPayload(r, &payload) err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil { if err != nil {
return httperror.BadRequest( return httperror.BadRequest(
"Invalid request payload", "Invalid request payload",
@ -74,7 +125,7 @@ func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.
err = cli.DeleteServices(payload) err = cli.DeleteServices(payload)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to delete service",
err, 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) err = cli.UpdateService(namespace, payload)
if err != nil { if err != nil {
return httperror.InternalServerError( return httperror.InternalServerError(
"Unable to retrieve nodes limits", "Unable to update service",
err, err,
) )
} }

View File

@ -5,8 +5,10 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"sync" "sync"
"time"
cmap "github.com/orcaman/concurrent-map" cmap "github.com/orcaman/concurrent-map"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors" "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
@ -23,6 +25,8 @@ type (
signatureService portainer.DigitalSignatureService signatureService portainer.DigitalSignatureService
instanceID string instanceID string
endpointClients cmap.ConcurrentMap endpointClients cmap.ConcurrentMap
endpointProxyClients *cache.Cache
AddrHTTPS string
} }
// KubeClient represent a service used to execute Kubernetes operations // KubeClient represent a service used to execute Kubernetes operations
@ -34,14 +38,24 @@ type (
) )
// NewClientFactory returns a new instance of a ClientFactory // 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{ return &ClientFactory{
dataStore: dataStore, dataStore: dataStore,
signatureService: signatureService, signatureService: signatureService,
reverseTunnelService: reverseTunnelService, reverseTunnelService: reverseTunnelService,
instanceID: instanceID, instanceID: instanceID,
endpointClients: cmap.New(), endpointClients: cmap.New(),
} endpointProxyClients: cache.New(timeout, timeout),
AddrHTTPS: addrHTTPS,
}, nil
} }
func (factory *ClientFactory) GetInstanceID() (instanceID string) { func (factory *ClientFactory) GetInstanceID() (instanceID string) {
@ -59,7 +73,7 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (porta
key := strconv.Itoa(int(endpoint.ID)) key := strconv.Itoa(int(endpoint.ID))
client, ok := factory.endpointClients.Get(key) client, ok := factory.endpointClients.Get(key)
if !ok { if !ok {
client, err := factory.createKubeClient(endpoint) client, err := factory.createCachedAdminKubeClient(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -71,7 +85,49 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (porta
return client.(portainer.KubeClient), nil 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) cli, err := factory.CreateClient(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -5,11 +5,12 @@ import (
"strings" "strings"
"github.com/portainer/portainer/api/database/models" "github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log"
netv1 "k8s.io/api/networking/v1" netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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 var controllers []models.K8sIngressController
// We know that each existing class points to a controller so we can start // 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() classClient := kcl.cli.NetworkingV1().IngressClasses()
classList, err := classClient.List(context.Background(), metav1.ListOptions{}) classList, err := classClient.List(context.Background(), metav1.ListOptions{})
if err != nil { if err != nil {
return nil return nil, err
} }
// We want to know which of these controllers is in use. // We want to know which of these controllers is in use.
var ingresses []models.K8sIngressInfo var ingresses []models.K8sIngressInfo
namespaces, err := kcl.GetNamespaces() namespaces, err := kcl.GetNamespaces()
if err != nil { if err != nil {
return nil return nil, err
} }
for namespace := range namespaces { for namespace := range namespaces {
t, err := kcl.GetIngresses(namespace) t, err := kcl.GetIngresses(namespace)
if err != nil { 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...) ingresses = append(ingresses, t...)
} }
@ -58,7 +62,7 @@ func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers {
} }
controllers = append(controllers, controller) controllers = append(controllers, controller)
} }
return controllers return controllers, nil
} }
// GetIngresses gets all the ingresses for a given namespace in a k8s endpoint. // 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) 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) 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"}, Resources: []string{"namespaces", "pods", "nodes"},
APIGroups: []string{"metrics.k8s.io"}, 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, "/")) baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
} }
log.Info(). log.Debug().
Str("host_URL", hostURL). Str("host_URL", hostURL).
Str("HTTPS_bind_address", service.httpsBindAddr). Str("HTTPS_bind_address", service.httpsBindAddr).
Str("base_URL", baseURL). Str("base_URL", baseURL).

View File

@ -1357,7 +1357,7 @@ type (
GetNamespaces() (map[string]K8sNamespaceInfo, error) GetNamespaces() (map[string]K8sNamespaceInfo, error)
DeleteNamespace(namespace string) error DeleteNamespace(namespace string) error
GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error)
GetIngressControllers() models.K8sIngressControllers GetIngressControllers() (models.K8sIngressControllers, error)
CreateIngress(namespace string, info models.K8sIngressInfo) error CreateIngress(namespace string, info models.K8sIngressInfo) error
UpdateIngress(namespace string, info models.K8sIngressInfo) error UpdateIngress(namespace string, info models.K8sIngressInfo) error
GetIngresses(namespace string) ([]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 { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
import { useNamespaces } from '@/react/kubernetes/namespaces/queries'; 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 { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
import { Datatable } from '@@/datatables'; import { Datatable } from '@@/datatables';
@ -55,6 +55,7 @@ export function IngressDataTable() {
}} }}
getRowId={(row) => row.Name + row.Type + row.Namespace} getRowId={(row) => row.Name + row.Type + row.Namespace}
renderTableActions={tableActions} renderTableActions={tableActions}
disableSelect={useCheckboxes()}
/> />
); );
@ -80,7 +81,7 @@ export function IngressDataTable() {
</Button> </Button>
</Authorized> </Authorized>
<Authorized authorizations="K8sIngressesAdd"> <Authorized authorizations="K8sIngressesW">
<Link to="kubernetes.ingresses.create" className="space-left"> <Link to="kubernetes.ingresses.create" className="space-left">
<Button <Button
icon={Plus} icon={Plus}
@ -91,7 +92,7 @@ export function IngressDataTable() {
</Button> </Button>
</Link> </Link>
</Authorized> </Authorized>
<Authorized authorizations="K8sApplicationsW"> <Authorized authorizations="K8sIngressesW">
<Link to="kubernetes.deploy" className="space-left"> <Link to="kubernetes.deploy" className="space-left">
<Button icon={Plus} className="btn-wrapper"> <Button icon={Plus} className="btn-wrapper">
Create from manifest Create from manifest
@ -102,6 +103,10 @@ export function IngressDataTable() {
); );
} }
function useCheckboxes() {
return !useAuthorizations(['K8sIngressesW']);
}
async function handleRemoveClick(ingresses: SelectedIngress[]) { async function handleRemoveClick(ingresses: SelectedIngress[]) {
const confirmed = await confirmDeletionAsync( const confirmed = await confirmDeletionAsync(
'Are you sure you want to delete the selected ingresses?' 'Are you sure you want to delete the selected ingresses?'

View File

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

View File

@ -11,7 +11,9 @@
</tr> </tr>
<tr ng-repeat="ingress in $ctrl.applicationIngress"> <tr ng-repeat="ingress in $ctrl.applicationIngress">
<td <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.ServiceName }}</td>
<td>{{ ingress.Host }}</td> <td>{{ ingress.Host }}</td>

View File

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