mirror of https://github.com/portainer/portainer
fix(kubernetes): create proxied kubeclient EE-4326 (#7850)
parent
f6d6be90e4
commit
0c995ae1c8
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue