mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
refactor(k8s): namespace core logic (#12142)
Co-authored-by: testA113 <aliharriss1995@gmail.com> Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io> Co-authored-by: James Carppe <85850129+jamescarppe@users.noreply.github.com> Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
This commit is contained in:
parent
da010f3d08
commit
ea228c3d6d
276 changed files with 9241 additions and 3361 deletions
|
@ -9,8 +9,6 @@ linters:
|
||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- errorlint
|
- errorlint
|
||||||
- copyloopvar
|
|
||||||
- intrange
|
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
depguard:
|
depguard:
|
||||||
|
|
|
@ -90,7 +90,7 @@ func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endp
|
||||||
switch {
|
switch {
|
||||||
case endpointutils.IsKubernetesEndpoint(environment):
|
case endpointutils.IsKubernetesEndpoint(environment):
|
||||||
// get the kubeclient for the environment, and skip all kube migrations if there's an error
|
// get the kubeclient for the environment, and skip all kube migrations if there's an error
|
||||||
kubeclient, err := migrator.kubeFactory.GetKubeClient(environment)
|
kubeclient, err := migrator.kubeFactory.GetPrivilegedKubeClient(environment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
|
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -44,7 +44,7 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
|
||||||
}
|
}
|
||||||
|
|
||||||
func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
||||||
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
kubeCLI, err := deployer.kubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, name
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "unable to retrieve kubernetes client")
|
return false, errors.Wrap(err, "unable to retrieve kubernetes client")
|
||||||
}
|
}
|
||||||
|
@ -187,7 +187,7 @@ func (handler *Handler) filterKubernetesRegistriesByUserRole(r *http.Request, re
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) userNamespaces(endpoint *portainer.Endpoint, user *portainer.User) ([]string, error) {
|
func (handler *Handler) userNamespaces(endpoint *portainer.Endpoint, user *portainer.User) ([]string, error) {
|
||||||
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,7 +134,7 @@ func (handler *Handler) updateKubeAccess(endpoint *portainer.Endpoint, registry
|
||||||
namespacesToRemove := setDifference(oldNamespacesSet, newNamespacesSet)
|
namespacesToRemove := setDifference(oldNamespacesSet, newNamespacesSet)
|
||||||
namespacesToAdd := setDifference(newNamespacesSet, oldNamespacesSet)
|
namespacesToAdd := setDifference(newNamespacesSet, oldNamespacesSet)
|
||||||
|
|
||||||
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
150
api/http/handler/kubernetes/application.go
Normal file
150
api/http/handler/kubernetes/application.go
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetApplicationsResources
|
||||||
|
// @summary Get the total resource requests and limits of all applications
|
||||||
|
// @description Get the total CPU (cores) and memory requests (MB) and limits of all applications across all namespaces.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment(Endpoint) identifier"
|
||||||
|
// @param node query string true "Node name"
|
||||||
|
// @success 200 {object} models.K8sApplicationResource "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the total resource requests and limits for all applications from the cluster."
|
||||||
|
// @router /kubernetes/{id}/metrics/applications_resources [get]
|
||||||
|
func (handler *Handler) getApplicationsResources(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
node, err := request.RetrieveQueryParameter(r, "node", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getApplicationsResources").Msg("Unable to parse the namespace query parameter")
|
||||||
|
return httperror.BadRequest("Unable to parse the node query parameter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "getApplicationsResources").Msg("Unable to prepare kube client")
|
||||||
|
return httperror.InternalServerError("Unable to prepare kube client", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationsResources, err := cli.GetApplicationsResource("", node)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getApplicationsResources").Msg("Unable to get the total resource requests and limits for all applications in the namespace")
|
||||||
|
return httperror.Unauthorized("Unable to get the total resource requests and limits for all applications in the namespace", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getApplicationsResources").Msg("Unable to get the total resource requests and limits for all applications in the namespace")
|
||||||
|
return httperror.Forbidden("Unable to get the total resource requests and limits for all applications in the namespace", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getApplicationsResources").Msg("Unable to calculate the total resource requests and limits for all applications in the namespace")
|
||||||
|
return httperror.InternalServerError("Unable to calculate the total resource requests and limits for all applications in the namespace", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, applicationsResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetAllKubernetesApplications
|
||||||
|
// @summary Get a list of applications across all namespaces in the cluster. If the nodeName is provided, it will return the applications running on that node.
|
||||||
|
// @description Get a list of applications across all namespaces in the cluster. If the nodeName is provided, it will return the applications running on that node.
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment(Endpoint) identifier"
|
||||||
|
// @param namespace query string true "Namespace name"
|
||||||
|
// @param nodeName query string true "Node name"
|
||||||
|
// @param withDependencies query boolean false "Include dependencies in the response"
|
||||||
|
// @success 200 {array} models.K8sApplication "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the list of applications from the cluster."
|
||||||
|
// @router /kubernetes/{id}/applications [get]
|
||||||
|
func (handler *Handler) GetAllKubernetesApplications(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
applications, err := handler.getAllKubernetesApplications(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, applications)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetAllKubernetesApplicationsCount
|
||||||
|
// @summary Get Applications count
|
||||||
|
// @description Get the count of Applications across all namespaces in the cluster. If the nodeName is provided, it will return the count of applications running on that node.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {integer} integer "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the count of all applications from the cluster."
|
||||||
|
// @router /kubernetes/{id}/applications/count [get]
|
||||||
|
func (handler *Handler) getAllKubernetesApplicationsCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
applications, err := handler.getAllKubernetesApplications(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, len(applications))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getAllKubernetesApplications(r *http.Request) ([]models.K8sApplication, *httperror.HandlerError) {
|
||||||
|
namespace, err := request.RetrieveQueryParameter(r, "namespace", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Msg("Unable to parse the namespace query parameter")
|
||||||
|
return nil, httperror.BadRequest("Unable to parse the namespace query parameter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
withDependencies, err := request.RetrieveBooleanQueryParameter(r, "withDependencies", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Msg("Unable to parse the withDependencies query parameter")
|
||||||
|
return nil, httperror.BadRequest("Unable to parse the withDependencies query parameter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeName, err := request.RetrieveQueryParameter(r, "nodeName", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Msg("Unable to parse the nodeName query parameter")
|
||||||
|
return nil, httperror.BadRequest("Unable to parse the nodeName query parameter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "getAllKubernetesApplications").Str("namespace", namespace).Str("nodeName", nodeName).Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return nil, httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
applications, err := cli.GetApplications(namespace, nodeName, withDependencies)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Str("namespace", namespace).Str("nodeName", nodeName).Msg("Unable to get the list of applications")
|
||||||
|
return nil, httperror.Unauthorized("Unable to get the list of applications", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Str("namespace", namespace).Str("nodeName", nodeName).Msg("Unable to get the list of applications")
|
||||||
|
return nil, httperror.InternalServerError("Unable to get the list of applications", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return applications, nil
|
||||||
|
}
|
37
api/http/handler/kubernetes/client.go
Normal file
37
api/http/handler/kubernetes/client.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/middlewares"
|
||||||
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// prepareKubeClient is a helper function to prepare a Kubernetes client for the user
|
||||||
|
// it first fetches getProxyKubeClient to grab the user's admin status and non admin namespaces
|
||||||
|
// then these two values are parsed to create a privileged client
|
||||||
|
func (handler *Handler) prepareKubeClient(r *http.Request) (*cli.KubeClient, *httperror.HandlerError) {
|
||||||
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr.Err).Str("context", "prepareKubeClient").Msg("Unable to get a Kubernetes client for the user.")
|
||||||
|
return nil, httperror.InternalServerError("Unable to get a Kubernetes client for the user.", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "prepareKubeClient").Msg("Unable to find the Kubernetes endpoint associated to the request.")
|
||||||
|
return nil, httperror.NotFound("Unable to find the Kubernetes endpoint associated to the request.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pcli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "prepareKubeClient").Msg("Unable to get a privileged Kubernetes client for the user.")
|
||||||
|
return nil, httperror.InternalServerError("Unable to get a privileged Kubernetes client for the user.", err)
|
||||||
|
}
|
||||||
|
pcli.IsKubeAdmin = cli.IsKubeAdmin
|
||||||
|
pcli.NonAdminNamespaces = cli.NonAdminNamespaces
|
||||||
|
|
||||||
|
return pcli, nil
|
||||||
|
}
|
45
api/http/handler/kubernetes/cluster_role_bindings.go
Normal file
45
api/http/handler/kubernetes/cluster_role_bindings.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetAllKubernetesClusterRoleBindings
|
||||||
|
// @summary Get a list of kubernetes cluster role bindings
|
||||||
|
// @description Get a list of kubernetes cluster role bindings within the given environment at the cluster level.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {array} kubernetes.K8sClusterRoleBinding "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the list of cluster role bindings."
|
||||||
|
// @router /kubernetes/{id}/clusterrolebindings [get]
|
||||||
|
func (handler *Handler) getAllKubernetesClusterRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr.Err).Str("context", "getAllKubernetesClusterRoleBindings").Msg("user is not authorized to fetch cluster role bindings from the Kubernetes cluster.")
|
||||||
|
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cli.IsKubeAdmin {
|
||||||
|
log.Error().Str("context", "getAllKubernetesClusterRoleBindings").Msg("user is not authorized to fetch cluster role bindings from the Kubernetes cluster.")
|
||||||
|
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterrolebindings, err := cli.GetClusterRoleBindings()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesClusterRoleBindings").Msg("Unable to fetch cluster role bindings.")
|
||||||
|
return httperror.InternalServerError("Unable to fetch cluster role bindings.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, clusterrolebindings)
|
||||||
|
}
|
45
api/http/handler/kubernetes/cluster_roles.go
Normal file
45
api/http/handler/kubernetes/cluster_roles.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetAllKubernetesClusterRoles
|
||||||
|
// @summary Get a list of kubernetes cluster roles
|
||||||
|
// @description Get a list of kubernetes cluster roles within the given environment at the cluster level.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {array} kubernetes.K8sClusterRole "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the list of cluster roles."
|
||||||
|
// @router /kubernetes/{id}/clusterroles [get]
|
||||||
|
func (handler *Handler) getAllKubernetesClusterRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr.Err).Str("context", "getAllKubernetesClusterRoles").Msg("user is not authorized to fetch cluster roles from the Kubernetes cluster.")
|
||||||
|
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cli.IsKubeAdmin {
|
||||||
|
log.Error().Str("context", "getAllKubernetesClusterRoles").Msg("user is not authorized to fetch cluster roles from the Kubernetes cluster.")
|
||||||
|
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterroles, err := cli.GetClusterRoles()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesClusterRoles").Msg("Unable to fetch clusterroles.")
|
||||||
|
return httperror.InternalServerError("Unable to fetch clusterroles.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, clusterroles)
|
||||||
|
}
|
|
@ -12,45 +12,48 @@ import (
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id GetKubernetesConfig
|
// @id GetKubernetesConfig
|
||||||
// @summary Generate a kubeconfig file enabling client communication with k8s api server
|
// @summary Generate a kubeconfig file
|
||||||
// @description Generate a kubeconfig file enabling client communication with k8s api server
|
// @description Generate a kubeconfig file that allows a client to communicate with the Kubernetes API server
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
// @produce application/json, application/yaml
|
||||||
// @accept json
|
|
||||||
// @produce json
|
|
||||||
// @param ids query []int false "will include only these environments(endpoints)"
|
// @param ids query []int false "will include only these environments(endpoints)"
|
||||||
// @param excludeIds query []int false "will exclude these environments(endpoints)"
|
// @param excludeIds query []int false "will exclude these environments(endpoints)"
|
||||||
// @success 200 "Success"
|
// @success 200 {object} interface{} "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 401 "Unauthorized"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
// @failure 403 "Permission denied"
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
// @failure 404 "Environment(Endpoint) or ServiceAccount not found"
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
// @failure 500 "Server error"
|
// @failure 500 "Server error occurred while attempting to generate the kubeconfig file."
|
||||||
// @router /kubernetes/config [get]
|
// @router /kubernetes/config [get]
|
||||||
func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesConfig").Msg("Permission denied to access environment")
|
||||||
return httperror.Forbidden("Permission denied to access environment", err)
|
return httperror.Forbidden("Permission denied to access environment", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
|
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesConfig").Msg("Unable to generate JWT token")
|
||||||
return httperror.InternalServerError("Unable to generate JWT token", err)
|
return httperror.InternalServerError("Unable to generate JWT token", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoints, handlerErr := handler.filterUserKubeEndpoints(r)
|
endpoints, handlerErr := handler.filterUserKubeEndpoints(r)
|
||||||
if handlerErr != nil {
|
if handlerErr != nil {
|
||||||
|
log.Error().Err(handlerErr).Str("context", "getKubernetesConfig").Msg("Unable to filter user kube endpoints")
|
||||||
return handlerErr
|
return handlerErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(endpoints) == 0 {
|
if len(endpoints) == 0 {
|
||||||
|
log.Error().Str("context", "getKubernetesConfig").Msg("Empty endpoints list")
|
||||||
return httperror.BadRequest("empty endpoints list", errors.New("empty endpoints list"))
|
return httperror.BadRequest("empty endpoints list", errors.New("empty endpoints list"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,16 +70,19 @@ func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.En
|
||||||
_ = request.RetrieveJSONQueryParameter(r, "excludeIds", &excludeEndpointIDs, true)
|
_ = request.RetrieveJSONQueryParameter(r, "excludeIds", &excludeEndpointIDs, true)
|
||||||
|
|
||||||
if len(endpointIDs) > 0 && len(excludeEndpointIDs) > 0 {
|
if len(endpointIDs) > 0 && len(excludeEndpointIDs) > 0 {
|
||||||
|
log.Error().Str("context", "filterUserKubeEndpoints").Msg("Can't provide both 'ids' and 'excludeIds' parameters")
|
||||||
return nil, httperror.BadRequest("Can't provide both 'ids' and 'excludeIds' parameters", errors.New("invalid parameters"))
|
return nil, httperror.BadRequest("Can't provide both 'ids' and 'excludeIds' parameters", errors.New("invalid parameters"))
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve info from request context")
|
||||||
return nil, httperror.InternalServerError("Unable to retrieve info from request context", err)
|
return nil, httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointGroups, err := handler.DataStore.EndpointGroup().ReadAll()
|
endpointGroups, err := handler.DataStore.EndpointGroup().ReadAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve environment groups from the database")
|
||||||
return nil, httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
|
return nil, httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +91,7 @@ func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.En
|
||||||
for _, endpointID := range endpointIDs {
|
for _, endpointID := range endpointIDs {
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve environment from the database")
|
||||||
return nil, httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
return nil, httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
||||||
}
|
}
|
||||||
if !endpointutils.IsKubernetesEndpoint(endpoint) {
|
if !endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||||
|
@ -101,6 +108,7 @@ func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.En
|
||||||
var kubeEndpoints []portainer.Endpoint
|
var kubeEndpoints []portainer.Endpoint
|
||||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve environments from the database")
|
||||||
return nil, httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
return nil, httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,6 +205,7 @@ func writeFileContent(w http.ResponseWriter, r *http.Request, endpoints []portai
|
||||||
if r.Header.Get("Accept") == "text/yaml" {
|
if r.Header.Get("Accept") == "text/yaml" {
|
||||||
yaml, err := kcli.GenerateYAML(config)
|
yaml, err := kcli.GenerateYAML(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "writeFileContent").Msg("Failed to generate Kubeconfig")
|
||||||
return httperror.InternalServerError("Failed to generate Kubeconfig", err)
|
return httperror.InternalServerError("Failed to generate Kubeconfig", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
159
api/http/handler/kubernetes/configmaps.go
Normal file
159
api/http/handler/kubernetes/configmaps.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetKubernetesConfigMap
|
||||||
|
// @summary Get a ConfigMap
|
||||||
|
// @description Get a ConfigMap by name for a given namespace.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param namespace path string true "The namespace name where the configmap is located"
|
||||||
|
// @param configmap path string true "The configmap name to get details for"
|
||||||
|
// @success 200 {object} models.K8sConfigMap "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or a configmap with the specified name in the given namespace."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve a configmap by name within the specified namespace."
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/configmaps/{configmap} [get]
|
||||||
|
func (handler *Handler) getKubernetesConfigMap(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
|
||||||
|
return httperror.BadRequest("Unable to retrieve namespace identifier route variable", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configMapName, err := request.RetrieveRouteVariableValue(r, "configmap")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Msg("Unable to retrieve configMap identifier route variable")
|
||||||
|
return httperror.BadRequest("Unable to retrieve configMap identifier route variable", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
configMap, err := cli.GetConfigMap(namespace, configMapName)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unable to retrieve configMap")
|
||||||
|
return httperror.NotFound("Unable to retrieve configMap", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unable to retrieve configMap")
|
||||||
|
return httperror.InternalServerError("Unable to retrieve configMap", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configMapWithApplications, err := cli.CombineConfigMapWithApplications(configMap)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unable to combine configMap with applications")
|
||||||
|
return httperror.InternalServerError("Unable to combine configMap with applications", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, configMapWithApplications)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetAllKubernetesConfigMaps
|
||||||
|
// @summary Get a list of ConfigMaps
|
||||||
|
// @description Get a list of ConfigMaps across all namespaces in the cluster. For non-admin users, it will only return ConfigMaps based on the namespaces that they have access to.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param isUsed query bool true "Set to true to include information about applications that use the ConfigMaps in the response"
|
||||||
|
// @success 200 {array} models.K8sConfigMap "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve all configmaps from the cluster."
|
||||||
|
// @router /kubernetes/{id}/configmaps [get]
|
||||||
|
func (handler *Handler) GetAllKubernetesConfigMaps(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
configMaps, err := handler.getAllKubernetesConfigMaps(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, configMaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetAllKubernetesConfigMapsCount
|
||||||
|
// @summary Get ConfigMaps count
|
||||||
|
// @description Get the count of ConfigMaps across all namespaces in the cluster. For non-admin users, it will only return the count of ConfigMaps based on the namespaces that they have access to.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {integer} integer "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the count of all configmaps from the cluster."
|
||||||
|
// @router /kubernetes/{id}/configmaps/count [get]
|
||||||
|
func (handler *Handler) getAllKubernetesConfigMapsCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
configMaps, err := handler.getAllKubernetesConfigMaps(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, len(configMaps))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getAllKubernetesConfigMaps(r *http.Request) ([]models.K8sConfigMap, *httperror.HandlerError) {
|
||||||
|
isUsed, err := request.RetrieveBooleanQueryParameter(r, "isUsed", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesConfigMaps").Msg("Unable to retrieve isUsed query parameter")
|
||||||
|
return nil, httperror.BadRequest("Unable to retrieve isUsed query parameter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "getAllKubernetesConfigMaps").Msg("Unable to prepare kube client")
|
||||||
|
return nil, httperror.InternalServerError("Unable to prepare kube client", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
configMaps, err := cli.GetConfigMaps("")
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesConfigMaps").Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return nil, httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesConfigMaps").Msg("Unable to get configMaps")
|
||||||
|
return nil, httperror.InternalServerError("Unable to get configMaps", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isUsed {
|
||||||
|
configMapsWithApplications, err := cli.CombineConfigMapsWithApplications(configMaps)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesConfigMaps").Msg("Unable to combine configMaps with associated applications")
|
||||||
|
return nil, httperror.InternalServerError("Unable to combine configMaps with associated applications", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configMapsWithApplications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return configMaps, nil
|
||||||
|
}
|
|
@ -1,44 +0,0 @@
|
||||||
package kubernetes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @id getKubernetesConfigMapsAndSecrets
|
|
||||||
// @summary Get ConfigMaps and Secrets
|
|
||||||
// @description Get all ConfigMaps and Secrets for a given namespace
|
|
||||||
// @description **Access policy**: authenticated
|
|
||||||
// @tags kubernetes
|
|
||||||
// @security ApiKeyAuth
|
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
|
||||||
// @param namespace path string true "Namespace name"
|
|
||||||
// @success 200 {array} []kubernetes.K8sConfigMapOrSecret "Success"
|
|
||||||
// @failure 400 "Invalid request"
|
|
||||||
// @failure 500 "Server error"
|
|
||||||
// @deprecated
|
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/configuration [get]
|
|
||||||
func (handler *Handler) getKubernetesConfigMapsAndSecrets(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
|
||||||
if err != nil {
|
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
|
||||||
if handlerErr != nil {
|
|
||||||
return handlerErr
|
|
||||||
}
|
|
||||||
|
|
||||||
configmaps, err := cli.GetConfigMapsAndSecrets(namespace)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to retrieve configmaps and secrets", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, configmaps)
|
|
||||||
}
|
|
|
@ -9,11 +9,10 @@ import (
|
||||||
|
|
||||||
// @id GetKubernetesDashboard
|
// @id GetKubernetesDashboard
|
||||||
// @summary Get the dashboard summary data
|
// @summary Get the dashboard summary data
|
||||||
// @description Get the dashboard summary data which is simply a count of a range of different commonly used kubernetes resources
|
// @description Get the dashboard summary data which is simply a count of a range of different commonly used kubernetes resources.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment (Endpoint) identifier"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -11,11 +11,11 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/middlewares"
|
"github.com/portainer/portainer/api/http/middlewares"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
|
||||||
"github.com/portainer/portainer/api/kubernetes"
|
"github.com/portainer/portainer/api/kubernetes"
|
||||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
@ -49,94 +49,98 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
||||||
// endpoints
|
// endpoints
|
||||||
endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter()
|
endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter()
|
||||||
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||||
endpointRouter.Use(kubeOnlyMiddleware)
|
|
||||||
endpointRouter.Use(h.kubeClientMiddleware)
|
endpointRouter.Use(h.kubeClientMiddleware)
|
||||||
|
|
||||||
|
endpointRouter.Handle("/applications", httperror.LoggerHandler(h.GetAllKubernetesApplications)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/applications/count", httperror.LoggerHandler(h.getAllKubernetesApplicationsCount)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/configmaps", httperror.LoggerHandler(h.GetAllKubernetesConfigMaps)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/configmaps/count", httperror.LoggerHandler(h.getAllKubernetesConfigMapsCount)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/cluster_roles", httperror.LoggerHandler(h.getAllKubernetesClusterRoles)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/cluster_role_bindings", httperror.LoggerHandler(h.getAllKubernetesClusterRoleBindings)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/configmaps", httperror.LoggerHandler(h.GetAllKubernetesConfigMaps)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/configmaps/count", httperror.LoggerHandler(h.getAllKubernetesConfigMapsCount)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/dashboard", httperror.LoggerHandler(h.getKubernetesDashboard)).Methods(http.MethodGet)
|
endpointRouter.Handle("/dashboard", httperror.LoggerHandler(h.getKubernetesDashboard)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/nodes_limits", httperror.LoggerHandler(h.getKubernetesNodesLimits)).Methods(http.MethodGet)
|
endpointRouter.Handle("/nodes_limits", httperror.LoggerHandler(h.getKubernetesNodesLimits)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/max_resource_limits", httperror.LoggerHandler(h.getKubernetesMaxResourceLimits)).Methods(http.MethodGet)
|
endpointRouter.Handle("/max_resource_limits", httperror.LoggerHandler(h.getKubernetesMaxResourceLimits)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/metrics/applications_resources", httperror.LoggerHandler(h.getApplicationsResources)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/metrics/nodes", httperror.LoggerHandler(h.getKubernetesMetricsForAllNodes)).Methods(http.MethodGet)
|
endpointRouter.Handle("/metrics/nodes", httperror.LoggerHandler(h.getKubernetesMetricsForAllNodes)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/metrics/nodes/{name}", httperror.LoggerHandler(h.getKubernetesMetricsForNode)).Methods(http.MethodGet)
|
endpointRouter.Handle("/metrics/nodes/{name}", httperror.LoggerHandler(h.getKubernetesMetricsForNode)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/metrics/pods/namespace/{namespace}", httperror.LoggerHandler(h.getKubernetesMetricsForAllPods)).Methods(http.MethodGet)
|
endpointRouter.Handle("/metrics/pods/namespace/{namespace}", httperror.LoggerHandler(h.getKubernetesMetricsForAllPods)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/metrics/pods/namespace/{namespace}/{name}", httperror.LoggerHandler(h.getKubernetesMetricsForPod)).Methods(http.MethodGet)
|
endpointRouter.Handle("/metrics/pods/namespace/{namespace}/{name}", httperror.LoggerHandler(h.getKubernetesMetricsForPod)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllers)).Methods(http.MethodGet)
|
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getAllKubernetesIngressControllers)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllers)).Methods(http.MethodPut)
|
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllers)).Methods(http.MethodPut)
|
||||||
endpointRouter.Handle("/ingresses/delete", httperror.LoggerHandler(h.deleteKubernetesIngresses)).Methods(http.MethodPost)
|
endpointRouter.Handle("/ingresses/delete", httperror.LoggerHandler(h.deleteKubernetesIngresses)).Methods(http.MethodPost)
|
||||||
|
endpointRouter.Handle("/ingresses", httperror.LoggerHandler(h.GetAllKubernetesClusterIngresses)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/ingresses/count", httperror.LoggerHandler(h.getAllKubernetesClusterIngressesCount)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/service_accounts", httperror.LoggerHandler(h.getAllKubernetesServiceAccounts)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/services", httperror.LoggerHandler(h.GetAllKubernetesServices)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/services/count", httperror.LoggerHandler(h.getAllKubernetesServicesCount)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/secrets", httperror.LoggerHandler(h.GetAllKubernetesSecrets)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/secrets/count", httperror.LoggerHandler(h.getAllKubernetesSecretsCount)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/services/delete", httperror.LoggerHandler(h.deleteKubernetesServices)).Methods(http.MethodPost)
|
endpointRouter.Handle("/services/delete", httperror.LoggerHandler(h.deleteKubernetesServices)).Methods(http.MethodPost)
|
||||||
endpointRouter.Handle("/rbac_enabled", httperror.LoggerHandler(h.isRBACEnabled)).Methods(http.MethodGet)
|
endpointRouter.Handle("/rbac_enabled", httperror.LoggerHandler(h.getKubernetesRBACStatus)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/roles", httperror.LoggerHandler(h.getAllKubernetesRoles)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/role_bindings", httperror.LoggerHandler(h.getAllKubernetesRoleBindings)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.createKubernetesNamespace)).Methods(http.MethodPost)
|
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.createKubernetesNamespace)).Methods(http.MethodPost)
|
||||||
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut)
|
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut)
|
||||||
|
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.deleteKubernetesNamespace)).Methods(http.MethodDelete)
|
||||||
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.getKubernetesNamespaces)).Methods(http.MethodGet)
|
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.getKubernetesNamespaces)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/namespace/{namespace}", httperror.LoggerHandler(h.deleteKubernetesNamespace)).Methods(http.MethodDelete)
|
endpointRouter.Handle("/namespaces/count", httperror.LoggerHandler(h.getKubernetesNamespacesCount)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/namespaces/{namespace}", httperror.LoggerHandler(h.getKubernetesNamespace)).Methods(http.MethodGet)
|
endpointRouter.Handle("/namespaces/{namespace}", httperror.LoggerHandler(h.getKubernetesNamespace)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/volumes", httperror.LoggerHandler(h.GetAllKubernetesVolumes)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/volumes/count", httperror.LoggerHandler(h.getAllKubernetesVolumesCount)).Methods(http.MethodGet)
|
||||||
|
|
||||||
// namespaces
|
// namespaces
|
||||||
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
|
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
|
||||||
// to keep it simple, we've decided to leave it like this.
|
// to keep it simple, we've decided to leave it like this.
|
||||||
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
||||||
|
namespaceRouter.Handle("/configmaps/{configmap}", httperror.LoggerHandler(h.getKubernetesConfigMap)).Methods(http.MethodGet)
|
||||||
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
|
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
|
||||||
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
|
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
|
||||||
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllersByNamespace)).Methods(http.MethodPut)
|
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllersByNamespace)).Methods(http.MethodPut)
|
||||||
namespaceRouter.Handle("/configuration", httperror.LoggerHandler(h.getKubernetesConfigMapsAndSecrets)).Methods(http.MethodGet)
|
namespaceRouter.Handle("/ingresses/{ingress}", httperror.LoggerHandler(h.getKubernetesIngress)).Methods(http.MethodGet)
|
||||||
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.createKubernetesIngress)).Methods(http.MethodPost)
|
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.createKubernetesIngress)).Methods(http.MethodPost)
|
||||||
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.updateKubernetesIngress)).Methods(http.MethodPut)
|
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.updateKubernetesIngress)).Methods(http.MethodPut)
|
||||||
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.getKubernetesIngresses)).Methods(http.MethodGet)
|
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.getKubernetesIngresses)).Methods(http.MethodGet)
|
||||||
|
namespaceRouter.Handle("/secrets/{secret}", httperror.LoggerHandler(h.getKubernetesSecret)).Methods(http.MethodGet)
|
||||||
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.createKubernetesService)).Methods(http.MethodPost)
|
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.createKubernetesService)).Methods(http.MethodPost)
|
||||||
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.updateKubernetesService)).Methods(http.MethodPut)
|
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.updateKubernetesService)).Methods(http.MethodPut)
|
||||||
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.getKubernetesServices)).Methods(http.MethodGet)
|
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.getKubernetesServicesByNamespace)).Methods(http.MethodGet)
|
||||||
|
namespaceRouter.Handle("/volumes/{volume}", httperror.LoggerHandler(h.getKubernetesVolume)).Methods(http.MethodGet)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
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.InternalServerError(
|
|
||||||
"Unable to find an environment on request context",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !endpointutils.IsKubernetesEndpoint(endpoint) {
|
|
||||||
errMessage := "environment is not a Kubernetes environment"
|
|
||||||
httperror.BadRequest(
|
|
||||||
errMessage,
|
|
||||||
errors.New(errMessage),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rw.Header().Set(portainer.PortainerCacheHeader, "true")
|
|
||||||
next.ServeHTTP(rw, request)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProxyKubeClient gets a kubeclient for the user. It's generally what you want as it retrieves the kubeclient
|
// getProxyKubeClient gets a kubeclient for the user. It's generally what you want as it retrieves the kubeclient
|
||||||
// from the Authorization token of the currently logged in user. The kubeclient that is not from the proxy is actually using
|
// from the Authorization token of the currently logged in user. The kubeclient that is not from the proxy is actually using
|
||||||
// admin permissions. If you're unsure which one to use, use this.
|
// admin permissions. If you're unsure which one to use, use this.
|
||||||
func (h *Handler) getProxyKubeClient(r *http.Request) (*cli.KubeClient, *httperror.HandlerError) {
|
func (h *Handler) getProxyKubeClient(r *http.Request) (*cli.KubeClient, *httperror.HandlerError) {
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httperror.BadRequest("Invalid environment identifier route variable", err)
|
return nil, httperror.BadRequest(fmt.Sprintf("an error occurred during the getProxyKubeClient operation, the environment identifier route variable is invalid for /api/kubernetes/%d. Error: ", endpointID), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httperror.Forbidden("Permission denied to access environment", err)
|
return nil, httperror.Forbidden(fmt.Sprintf("an error occurred during the getProxyKubeClient operation, permission denied to access the environment /api/kubernetes/%d. Error: ", endpointID), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, ok := h.KubernetesClientFactory.GetProxyKubeClient(strconv.Itoa(endpointID), tokenData.Token)
|
cli, ok := h.KubernetesClientFactory.GetProxyKubeClient(strconv.Itoa(endpointID), tokenData.Token)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, httperror.InternalServerError("Failed to lookup KubeClient", nil)
|
return nil, httperror.InternalServerError("an error occurred during the getProxyKubeClient operation,failed to get proxy KubeClient", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli, nil
|
return cli, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// kubeClientMiddleware is a middleware that will create a kubeclient for the user if it doesn't exist
|
||||||
|
// and store it in the factory for future use.
|
||||||
|
// if there is a kubeclient against this auth token already, the existing one will be reused.
|
||||||
|
// otherwise, generate a new one
|
||||||
func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
|
func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set(portainer.PortainerCacheHeader, "true")
|
||||||
|
|
||||||
if handler.KubernetesClientFactory == nil {
|
if handler.KubernetesClientFactory == nil {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
@ -144,13 +148,13 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
|
||||||
|
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteError(w, http.StatusBadRequest, "Invalid environment identifier route variable", err)
|
httperror.WriteError(w, http.StatusBadRequest, fmt.Sprintf("an error occurred during the KubeClientMiddleware operation, the environment identifier route variable is invalid for /api/kubernetes/%d. Error: ", endpointID), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteError(w, http.StatusForbidden, "Permission denied to access environment", err)
|
httperror.WriteError(w, http.StatusForbidden, "an error occurred during the KubeClientMiddleware operation, permission denied to access the environment. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have a kubeclient against this auth token already, otherwise generate a new one
|
// Check if we have a kubeclient against this auth token already, otherwise generate a new one
|
||||||
|
@ -163,35 +167,60 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||||
httperror.WriteError(
|
httperror.WriteError(w, http.StatusNotFound,
|
||||||
w,
|
"an error occurred during the KubeClientMiddleware operation, unable to find an environment with the specified environment identifier inside the database. Error: ", err)
|
||||||
http.StatusNotFound,
|
|
||||||
"Unable to find an environment with the specified identifier inside the database",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
httperror.WriteError(w, http.StatusInternalServerError, "Unable to read the environment from the database", err)
|
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, error reading from the Portainer database. Error: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user, err := security.RetrieveUserFromRequest(r, handler.DataStore)
|
||||||
|
if err != nil {
|
||||||
|
httperror.InternalServerError("an error occurred during the KubeClientMiddleware operation, unable to retrieve the user from request. Error: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.
|
||||||
|
Debug().
|
||||||
|
Str("context", "KubeClientMiddleware").
|
||||||
|
Str("endpoint", endpoint.Name).
|
||||||
|
Str("user", user.Username).
|
||||||
|
Msg("Creating a Kubernetes client")
|
||||||
|
|
||||||
|
isKubeAdmin := true
|
||||||
|
nonAdminNamespaces := []string{}
|
||||||
|
if user.Role != portainer.AdministratorRole {
|
||||||
|
pcli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to get privileged kube client to grab all namespaces. Error: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaces, err = pcli.GetNonAdminNamespaces(int(user.ID))
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to retrieve non-admin namespaces. Error: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isKubeAdmin = false
|
||||||
|
}
|
||||||
|
|
||||||
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
|
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteError(w, http.StatusInternalServerError, "Unable to create JWT token", err)
|
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to generate token for kubeconfig. Error: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config := handler.buildConfig(r, tokenData, bearerToken, []portainer.Endpoint{*endpoint}, true)
|
config := handler.buildConfig(r, tokenData, bearerToken, []portainer.Endpoint{*endpoint}, true)
|
||||||
if len(config.Clusters) == 0 {
|
if len(config.Clusters) == 0 {
|
||||||
httperror.WriteError(w, http.StatusInternalServerError, "Unable build cluster kubeconfig", nil)
|
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to build kubeconfig. Error: ", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually setting serverURL to localhost to route the request to proxy server
|
// Manually setting serverURL to localhost to route the request to proxy server
|
||||||
serverURL, err := url.Parse(config.Clusters[0].Cluster.Server)
|
serverURL, err := url.Parse(config.Clusters[0].Cluster.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteError(w, http.StatusInternalServerError, "Unable parse cluster's kubeconfig server URL", nil)
|
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to parse server URL for building kubeconfig. Error: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serverURL.Scheme = "https"
|
serverURL.Scheme = "https"
|
||||||
|
@ -200,17 +229,12 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
|
||||||
|
|
||||||
yaml, err := cli.GenerateYAML(config)
|
yaml, err := cli.GenerateYAML(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteError(
|
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to generate kubeconfig YAML. Error: ", err)
|
||||||
w,
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
"Unable to generate yaml from endpoint kubeconfig",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
kubeCli, err := handler.KubernetesClientFactory.CreateKubeClientFromKubeConfig(endpoint.Name, []byte(yaml))
|
kubeCli, err := handler.KubernetesClientFactory.CreateKubeClientFromKubeConfig(endpoint.Name, []byte(yaml), isKubeAdmin, nonAdminNamespaces)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteError(w, http.StatusInternalServerError, "Failed to create client from kubeconfig", err)
|
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to create kubernetes client from kubeconfig. Error: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,67 +10,65 @@ import (
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id getKubernetesIngressControllers
|
// @id GetAllKubernetesIngressControllers
|
||||||
// @summary Get a list of ingress controllers
|
// @summary Get a list of ingress controllers
|
||||||
// @description Get a list of ingress controllers for the given environment
|
// @description Get a list of ingress controllers for the given environment. If the allowedOnly query parameter is set, only ingress controllers that are allowed by the environment's ingress configuration will be returned.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param allowedOnly query boolean false "Only return allowed ingress controllers"
|
// @param allowedOnly query boolean false "Only return allowed ingress controllers"
|
||||||
// @success 200 {object} models.K8sIngressControllers "Success"
|
// @success 200 {object} models.K8sIngressControllers "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve ingress controllers"
|
||||||
// @router /kubernetes/{id}/ingresscontrollers [get]
|
// @router /kubernetes/{id}/ingresscontrollers [get]
|
||||||
func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getAllKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest(
|
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Invalid environment identifier route variable")
|
||||||
"Invalid environment identifier route variable",
|
return httperror.BadRequest("Invalid environment identifier route variable", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err != nil {
|
||||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||||
return httperror.NotFound(
|
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to find an environment with the specified identifier inside the database")
|
||||||
"Unable to find an environment with the specified identifier inside the database",
|
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
|
||||||
err,
|
}
|
||||||
)
|
|
||||||
} else if err != nil {
|
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to find an environment with the specified identifier inside the database")
|
||||||
return httperror.InternalServerError(
|
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
|
||||||
"Unable to find an environment with the specified identifier inside the database",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allowedOnly, err := request.RetrieveBooleanQueryParameter(r, "allowedOnly", true)
|
allowedOnly, err := request.RetrieveBooleanQueryParameter(r, "allowedOnly", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest(
|
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to retrieve allowedOnly query parameter")
|
||||||
"Invalid allowedOnly boolean query parameter",
|
return httperror.BadRequest("Unable to retrieve allowedOnly query parameter", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(
|
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to get privileged kube client")
|
||||||
"Unable to create Kubernetes client",
|
return httperror.InternalServerError("Unable to get privileged kube client", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controllers, err := cli.GetIngressControllers()
|
controllers, err := cli.GetIngressControllers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
"Failed to fetch ingressclasses",
|
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unauthorized access to the Kubernetes API")
|
||||||
err,
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to retrieve ingress controllers from the Kubernetes")
|
||||||
|
return httperror.InternalServerError("Unable to retrieve ingress controllers from the Kubernetes", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add none controller if "AllowNone" is set for endpoint.
|
// Add none controller if "AllowNone" is set for endpoint.
|
||||||
|
@ -82,16 +80,17 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
var updatedClasses []portainer.KubernetesIngressClassConfig
|
updatedClasses := []portainer.KubernetesIngressClassConfig{}
|
||||||
for i := range controllers {
|
for i := range controllers {
|
||||||
controllers[i].Availability = true
|
controllers[i].Availability = true
|
||||||
if controllers[i].ClassName != "none" {
|
if controllers[i].ClassName != "none" {
|
||||||
controllers[i].New = true
|
controllers[i].New = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedClass portainer.KubernetesIngressClassConfig
|
updatedClass := portainer.KubernetesIngressClassConfig{
|
||||||
updatedClass.Name = controllers[i].ClassName
|
Name: controllers[i].ClassName,
|
||||||
updatedClass.Type = controllers[i].Type
|
Type: controllers[i].Type,
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the controller is already known.
|
// Check if the controller is already known.
|
||||||
for _, existingClass := range existingClasses {
|
for _, existingClass := range existingClasses {
|
||||||
|
@ -112,16 +111,14 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
|
||||||
endpoint,
|
endpoint,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(
|
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to store found IngressClasses inside the database")
|
||||||
"Unable to store found IngressClasses inside the database",
|
return httperror.InternalServerError("Unable to store found IngressClasses inside the database", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the allowedOnly query parameter was set. We need to prune out
|
// If the allowedOnly query parameter was set. We need to prune out
|
||||||
// disallowed controllers from the response.
|
// disallowed controllers from the response.
|
||||||
if allowedOnly {
|
if allowedOnly {
|
||||||
var allowedControllers models.K8sIngressControllers
|
allowedControllers := models.K8sIngressControllers{}
|
||||||
for _, controller := range controllers {
|
for _, controller := range controllers {
|
||||||
if controller.Availability {
|
if controller.Availability {
|
||||||
allowedControllers = append(allowedControllers, controller)
|
allowedControllers = append(allowedControllers, controller)
|
||||||
|
@ -132,62 +129,61 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
|
||||||
return response.JSON(w, controllers)
|
return response.JSON(w, controllers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id getKubernetesIngressControllersByNamespace
|
// @id GetKubernetesIngressControllersByNamespace
|
||||||
// @summary Get a list ingress controllers by namespace
|
// @summary Get a list ingress controllers by namespace
|
||||||
// @description Get a list of ingress controllers for the given environment in the provided namespace
|
// @description Get a list of ingress controllers for the given environment in the provided namespace.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace"
|
// @param namespace path string true "Namespace"
|
||||||
// @success 200 {object} models.K8sIngressControllers "Success"
|
// @success 200 {object} models.K8sIngressControllers "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or a namespace with the specified name."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve ingress controllers by a namespace"
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [get]
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [get]
|
||||||
func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest(
|
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to retrieve environment identifier from request")
|
||||||
"Invalid environment identifier route variable",
|
return httperror.BadRequest("Unable to retrieve environment identifier from request", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err != nil {
|
||||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||||
return httperror.NotFound(
|
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to find an environment with the specified identifier inside the database")
|
||||||
"Unable to find an environment with the specified identifier inside the database",
|
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
|
||||||
err,
|
}
|
||||||
)
|
|
||||||
} else if err != nil {
|
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to find an environment with the specified identifier inside the database")
|
||||||
return httperror.InternalServerError(
|
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
|
||||||
"Unable to find an environment with the specified identifier inside the database",
|
}
|
||||||
err,
|
|
||||||
)
|
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to create Kubernetes client")
|
||||||
|
return httperror.InternalServerError("Unable to create Kubernetes client", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest(
|
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to retrieve namespace from request")
|
||||||
"Invalid namespace identifier route variable",
|
return httperror.BadRequest("Unable to retrieve namespace from request", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
|
||||||
if handlerErr != nil {
|
|
||||||
return handlerErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentControllers, err := cli.GetIngressControllers()
|
currentControllers, err := cli.GetIngressControllers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
"Failed to fetch ingressclasses",
|
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
|
||||||
err,
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Str("namespace", namespace).Msg("Unable to retrieve ingress controllers from the Kubernetes")
|
||||||
|
return httperror.InternalServerError("Unable to retrieve ingress controllers from the Kubernetes", err)
|
||||||
}
|
}
|
||||||
// Add none controller if "AllowNone" is set for endpoint.
|
// Add none controller if "AllowNone" is set for endpoint.
|
||||||
if endpoint.Kubernetes.Configuration.AllowNoneIngressClass {
|
if endpoint.Kubernetes.Configuration.AllowNoneIngressClass {
|
||||||
|
@ -197,21 +193,24 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon
|
||||||
Type: "custom",
|
Type: "custom",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
kubernetesConfig := endpoint.Kubernetes.Configuration
|
kubernetesConfig := endpoint.Kubernetes.Configuration
|
||||||
existingClasses := kubernetesConfig.IngressClasses
|
existingClasses := kubernetesConfig.IngressClasses
|
||||||
ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace
|
ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace
|
||||||
var updatedClasses []portainer.KubernetesIngressClassConfig
|
updatedClasses := []portainer.KubernetesIngressClassConfig{}
|
||||||
var controllers models.K8sIngressControllers
|
controllers := models.K8sIngressControllers{}
|
||||||
|
|
||||||
for i := range currentControllers {
|
for i := range currentControllers {
|
||||||
var globallyblocked bool
|
globallyblocked := false
|
||||||
currentControllers[i].Availability = true
|
currentControllers[i].Availability = true
|
||||||
if currentControllers[i].ClassName != "none" {
|
if currentControllers[i].ClassName != "none" {
|
||||||
currentControllers[i].New = true
|
currentControllers[i].New = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedClass portainer.KubernetesIngressClassConfig
|
updatedClass := portainer.KubernetesIngressClassConfig{
|
||||||
updatedClass.Name = currentControllers[i].ClassName
|
Name: currentControllers[i].ClassName,
|
||||||
updatedClass.Type = currentControllers[i].Type
|
Type: currentControllers[i].Type,
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the controller is blocked globally or in the current
|
// Check if the controller is blocked globally or in the current
|
||||||
// namespace.
|
// namespace.
|
||||||
|
@ -243,81 +242,77 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon
|
||||||
// Update the database to match the list of found controllers.
|
// Update the database to match the list of found controllers.
|
||||||
// This includes pruning out controllers which no longer exist.
|
// This includes pruning out controllers which no longer exist.
|
||||||
endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
|
endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
|
||||||
err = handler.DataStore.Endpoint().UpdateEndpoint(
|
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
||||||
portainer.EndpointID(endpointID),
|
|
||||||
endpoint,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(
|
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to store found IngressClasses inside the database")
|
||||||
"Unable to store found IngressClasses inside the database",
|
return httperror.InternalServerError("Unable to store found IngressClasses inside the database", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return response.JSON(w, controllers)
|
return response.JSON(w, controllers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id updateKubernetesIngressControllers
|
// @id UpdateKubernetesIngressControllers
|
||||||
// @summary Update (block/unblock) ingress controllers
|
// @summary Update (block/unblock) ingress controllers
|
||||||
// @description Update (block/unblock) ingress controllers
|
// @description Update (block/unblock) ingress controllers for the provided environment.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param body body []models.K8sIngressControllers true "Ingress controllers"
|
// @param body body models.K8sIngressControllers true "Ingress controllers"
|
||||||
// @success 200 {string} string "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or unable to find the ingress controllers to update."
|
||||||
|
// @failure 500 "Server error occurred while attempting to update ingress controllers."
|
||||||
// @router /kubernetes/{id}/ingresscontrollers [put]
|
// @router /kubernetes/{id}/ingresscontrollers [put]
|
||||||
func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest(
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to retrieve environment identifier from request")
|
||||||
"Invalid environment identifier route variable",
|
return httperror.BadRequest("Unable to retrieve environment identifier from request", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err != nil {
|
||||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||||
return httperror.NotFound(
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to find an environment with the specified identifier inside the database")
|
||||||
"Unable to find an environment with the specified identifier inside the database",
|
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
} else if err != nil {
|
|
||||||
return httperror.InternalServerError(
|
|
||||||
"Unable to find an environment with the specified identifier inside the database",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sIngressControllers
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to find an environment with the specified identifier inside the database")
|
||||||
|
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := models.K8sIngressControllers{}
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest(
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to decode and validate the request payload")
|
||||||
"Invalid request payload",
|
return httperror.BadRequest("Unable to decode and validate the request payload", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to get privileged kube client")
|
||||||
"Unable to create Kubernetes client",
|
return httperror.InternalServerError("Unable to get privileged kube client", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
controllers, err := cli.GetIngressControllers()
|
controllers, err := cli.GetIngressControllers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
"Unable to get ingress controllers",
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unauthorized access to the Kubernetes API")
|
||||||
err,
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to retrieve ingress controllers from the Kubernetes")
|
||||||
|
return httperror.NotFound("Unable to retrieve ingress controllers from the Kubernetes", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to retrieve ingress controllers from the Kubernetes")
|
||||||
|
return httperror.InternalServerError("Unable to retrieve ingress controllers from the Kubernetes", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add none controller if "AllowNone" is set for endpoint.
|
// Add none controller if "AllowNone" is set for endpoint.
|
||||||
|
@ -329,14 +324,15 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedClasses []portainer.KubernetesIngressClassConfig
|
updatedClasses := []portainer.KubernetesIngressClassConfig{}
|
||||||
for i := range controllers {
|
for i := range controllers {
|
||||||
controllers[i].Availability = true
|
controllers[i].Availability = true
|
||||||
controllers[i].New = true
|
controllers[i].New = true
|
||||||
|
|
||||||
var updatedClass portainer.KubernetesIngressClassConfig
|
updatedClass := portainer.KubernetesIngressClassConfig{
|
||||||
updatedClass.Name = controllers[i].ClassName
|
Name: controllers[i].ClassName,
|
||||||
updatedClass.Type = controllers[i].Type
|
Type: controllers[i].Type,
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the controller is already known.
|
// Check if the controller is already known.
|
||||||
for _, existingClass := range existingClasses {
|
for _, existingClass := range existingClasses {
|
||||||
|
@ -366,59 +362,64 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter
|
||||||
endpoint,
|
endpoint,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to store found IngressClasses inside the database")
|
||||||
"Unable to update the BlockedIngressClasses inside the database",
|
return httperror.InternalServerError("Unable to store found IngressClasses inside the database", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id updateKubernetesIngressControllersByNamespace
|
// @id UpdateKubernetesIngressControllersByNamespace
|
||||||
// @summary Update (block/unblock) ingress controllers by namespace
|
// @summary Update (block/unblock) ingress controllers by namespace
|
||||||
// @description Update (block/unblock) ingress controllers by namespace for the provided environment
|
// @description Update (block/unblock) ingress controllers by namespace for the provided environment.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace name"
|
// @param namespace path string true "Namespace name"
|
||||||
// @param body body []models.K8sIngressControllers true "Ingress controllers"
|
// @param body body models.K8sIngressControllers true "Ingress controllers"
|
||||||
// @success 200 {string} string "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to update ingress controllers by namespace."
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [put]
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [put]
|
||||||
func (handler *Handler) updateKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoint, err := middlewares.FetchEndpoint(r)
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.NotFound("Unable to find an environment on request context", err)
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllersByNamespace").Msg("Unable to fetch endpoint")
|
||||||
|
return httperror.NotFound("Unable to fetch endpoint", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllersByNamespace").Msg("Unable to retrieve namespace from request")
|
||||||
|
return httperror.BadRequest("Unable to retrieve namespace from request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sIngressControllers
|
payload := models.K8sIngressControllers{}
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllersByNamespace").Str("namespace", namespace).Msg("Unable to decode and validate the request payload")
|
||||||
|
return httperror.BadRequest("Unable to decode and validate the request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
var updatedClasses []portainer.KubernetesIngressClassConfig
|
updatedClasses := []portainer.KubernetesIngressClassConfig{}
|
||||||
PayloadLoop:
|
PayloadLoop:
|
||||||
for _, p := range payload {
|
for _, p := range payload {
|
||||||
for _, existingClass := range existingClasses {
|
for _, existingClass := range existingClasses {
|
||||||
if p.ClassName != existingClass.Name {
|
if p.ClassName != existingClass.Name {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var updatedClass portainer.KubernetesIngressClassConfig
|
updatedClass := portainer.KubernetesIngressClassConfig{
|
||||||
updatedClass.Name = existingClass.Name
|
Name: existingClass.Name,
|
||||||
updatedClass.Type = existingClass.Type
|
Type: existingClass.Type,
|
||||||
updatedClass.GloballyBlocked = existingClass.GloballyBlocked
|
GloballyBlocked: existingClass.GloballyBlocked,
|
||||||
|
}
|
||||||
|
|
||||||
// Handle "allow"
|
// Handle "allow"
|
||||||
if p.Availability {
|
if p.Availability {
|
||||||
|
@ -445,10 +446,7 @@ PayloadLoop:
|
||||||
continue PayloadLoop
|
continue PayloadLoop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updatedClass.BlockedNamespaces = append(
|
updatedClass.BlockedNamespaces = append(updatedClass.BlockedNamespaces, namespace)
|
||||||
updatedClass.BlockedNamespaces,
|
|
||||||
namespace,
|
|
||||||
)
|
|
||||||
updatedClasses = append(updatedClasses, updatedClass)
|
updatedClasses = append(updatedClasses, updatedClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -458,7 +456,7 @@ PayloadLoop:
|
||||||
// part of updatedClasses, but we MUST include it or we would remove the
|
// part of updatedClasses, but we MUST include it or we would remove the
|
||||||
// global block.
|
// global block.
|
||||||
for _, existingClass := range existingClasses {
|
for _, existingClass := range existingClasses {
|
||||||
var found bool
|
found := false
|
||||||
|
|
||||||
for _, updatedClass := range updatedClasses {
|
for _, updatedClass := range updatedClasses {
|
||||||
if existingClass.Name == updatedClass.Name {
|
if existingClass.Name == updatedClass.Name {
|
||||||
|
@ -474,32 +472,125 @@ PayloadLoop:
|
||||||
|
|
||||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to update the BlockedIngressClasses inside the database", err)
|
log.Error().Err(err).Str("context", "updateKubernetesIngressControllersByNamespace").Str("namespace", namespace).Msg("Unable to store BlockedIngressClasses inside the database")
|
||||||
|
return httperror.InternalServerError("Unable to store BlockedIngressClasses inside the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id getKubernetesIngresses
|
// @id GetAllKubernetesClusterIngresses
|
||||||
// @summary Get kubernetes ingresses by namespace
|
// @summary Get kubernetes ingresses at the cluster level
|
||||||
// @description Get kubernetes ingresses by namespace for the provided environment
|
// @description Get kubernetes ingresses at the cluster level for the provided environment.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param withServices query boolean false "Lookup services associated with each ingress"
|
||||||
|
// @success 200 {array} models.K8sIngressInfo "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve ingresses."
|
||||||
|
// @router /kubernetes/{id}/ingresses [get]
|
||||||
|
func (handler *Handler) GetAllKubernetesClusterIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
ingresses, err := handler.getKubernetesClusterIngresses(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, ingresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetAllKubernetesClusterIngressesCount
|
||||||
|
// @summary Get Ingresses count
|
||||||
|
// @description Get the number of kubernetes ingresses within the given environment.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {integer} integer "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve ingresses count."
|
||||||
|
// @router /kubernetes/{id}/ingresses/count [get]
|
||||||
|
func (handler *Handler) getAllKubernetesClusterIngressesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
ingresses, err := handler.getKubernetesClusterIngresses(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, len(ingresses))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getKubernetesClusterIngresses(r *http.Request) ([]models.K8sIngressInfo, *httperror.HandlerError) {
|
||||||
|
withServices, err := request.RetrieveBooleanQueryParameter(r, "withServices", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unable to retrieve withApplications query parameter")
|
||||||
|
return nil, httperror.BadRequest("Unable to retrieve withApplications query parameter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "getKubernetesClusterIngresses").Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return nil, httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
ingresses, err := cli.GetIngresses("")
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return nil, httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unable to retrieve ingresses from the Kubernetes for a cluster level user")
|
||||||
|
return nil, httperror.NotFound("Unable to retrieve ingresses from the Kubernetes for a cluster level user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unable to retrieve ingresses from the Kubernetes for a cluster level user")
|
||||||
|
return nil, httperror.InternalServerError("Unable to retrieve ingresses from the Kubernetes for a cluster level user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if withServices {
|
||||||
|
ingressesWithServices, err := cli.CombineIngressesWithServices(ingresses)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unable to combine ingresses with services")
|
||||||
|
return nil, httperror.InternalServerError("Unable to combine ingresses with services", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ingressesWithServices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ingresses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetAllKubernetesIngresses
|
||||||
|
// @summary Get a list of Ingresses
|
||||||
|
// @description Get a list of Ingresses. If namespace is provided, it will return the list of Ingresses in that namespace.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace name"
|
// @param namespace path string true "Namespace name"
|
||||||
// @param body body []models.K8sIngressInfo true "Ingress details"
|
// @success 200 {array} models.K8sIngressInfo "Success"
|
||||||
// @success 200 {string} string "Success"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 400 "Invalid request"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
// @failure 500 "Server error"
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve ingresses"
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [get]
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [get]
|
||||||
func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
log.Error().Err(err).Str("context", "getKubernetesIngresses").Msg("Unable to retrieve namespace from request")
|
||||||
|
return httperror.BadRequest("Unable to retrieve namespace from request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
cli, handlerErr := handler.getProxyKubeClient(r)
|
||||||
|
@ -509,38 +600,103 @@ func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Re
|
||||||
|
|
||||||
ingresses, err := cli.GetIngresses(namespace)
|
ingresses, err := cli.GetIngresses(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve ingresses", err)
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesIngresses").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesIngresses").Str("namespace", namespace).Msg("Unable to retrieve ingresses from the Kubernetes for a namespace level user")
|
||||||
|
return httperror.InternalServerError("Unable to retrieve ingresses from the Kubernetes for a namespace level user", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, ingresses)
|
return response.JSON(w, ingresses)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id createKubernetesIngress
|
// @id GetKubernetesIngress
|
||||||
// @summary Create a kubernetes ingress by namespace
|
// @summary Get an Ingress by name
|
||||||
// @description Create a kubernetes ingress by namespace for the provided environment
|
// @description Get an Ingress by name for the provided environment.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param namespace path string true "Namespace name"
|
||||||
|
// @param ingress path string true "Ingress name"
|
||||||
|
// @success 200 {object} models.K8sIngressInfo "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or unable to find an ingress with the specified name."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve an ingress."
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses/{ingress} [get]
|
||||||
|
func (handler *Handler) getKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesIngress").Msg("Unable to retrieve namespace from request")
|
||||||
|
return httperror.BadRequest("Unable to retrieve namespace from request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ingressName, err := request.RetrieveRouteVariableValue(r, "ingress")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesIngress").Msg("Unable to retrieve ingress from request")
|
||||||
|
return httperror.BadRequest("Unable to retrieve ingress from request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, handlerErr := handler.getProxyKubeClient(r)
|
||||||
|
if handlerErr != nil {
|
||||||
|
return handlerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
ingress, err := cli.GetIngress(namespace, ingressName)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesIngress").Str("namespace", namespace).Str("ingress", ingressName).Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesIngress").Str("namespace", namespace).Str("ingress", ingressName).Msg("Unable to retrieve ingress from the Kubernetes for a namespace level user")
|
||||||
|
return httperror.NotFound("Unable to retrieve ingress from the Kubernetes for a namespace level user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesIngress").Str("namespace", namespace).Str("ingress", ingressName).Msg("Unable to retrieve ingress from the Kubernetes for a namespace level user")
|
||||||
|
return httperror.InternalServerError("Unable to retrieve ingress from the Kubernetes for a namespace level user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, ingress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id CreateKubernetesIngress
|
||||||
|
// @summary Create an Ingress
|
||||||
|
// @description Create an Ingress for the provided environment.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace name"
|
// @param namespace path string true "Namespace name"
|
||||||
// @param body body models.K8sIngressInfo true "Ingress details"
|
// @param body body models.K8sIngressInfo true "Ingress details"
|
||||||
// @success 200 {string} string "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 409 "Conflict - an ingress with the same name already exists in the specified namespace."
|
||||||
|
// @failure 500 "Server error occurred while attempting to create an ingress."
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [post]
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [post]
|
||||||
func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
log.Error().Err(err).Str("context", "createKubernetesIngress").Msg("Unable to retrieve namespace from request")
|
||||||
|
return httperror.BadRequest("Unable to retrieve namespace from request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sIngressInfo
|
payload := models.K8sIngressInfo{}
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
log.Error().Err(err).Str("context", "createKubernetesIngress").Msg("Unable to decode and validate the request payload")
|
||||||
|
return httperror.BadRequest("Unable to decode and validate the request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
owner := "admin"
|
owner := "admin"
|
||||||
|
@ -556,26 +712,39 @@ func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.R
|
||||||
|
|
||||||
err = cli.CreateIngress(namespace, payload, owner)
|
err = cli.CreateIngress(namespace, payload, owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve the ingress", err)
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "createKubernetesIngress").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsAlreadyExists(err) {
|
||||||
|
log.Error().Err(err).Str("context", "createKubernetesIngress").Str("namespace", namespace).Msg("Ingress already exists")
|
||||||
|
return httperror.Conflict("Ingress already exists", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "createKubernetesIngress").Str("namespace", namespace).Msg("Unable to create an ingress")
|
||||||
|
return httperror.InternalServerError("Unable to create an ingress", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id deleteKubernetesIngresses
|
// @id DeleteKubernetesIngresses
|
||||||
// @summary Delete kubernetes ingresses
|
// @summary Delete one or more Ingresses
|
||||||
// @description Delete kubernetes ingresses for the provided environment
|
// @description Delete one or more Ingresses in the provided environment.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param body body models.K8sIngressDeleteRequests true "Ingress details"
|
// @param body body models.K8sIngressDeleteRequests true "Ingress details"
|
||||||
// @success 200 {string} string "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific ingress."
|
||||||
|
// @failure 500 "Server error occurred while attempting to delete specified ingresses."
|
||||||
// @router /kubernetes/{id}/ingresses/delete [post]
|
// @router /kubernetes/{id}/ingresses/delete [post]
|
||||||
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, handlerErr := handler.getProxyKubeClient(r)
|
cli, handlerErr := handler.getProxyKubeClient(r)
|
||||||
|
@ -583,46 +752,62 @@ func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http
|
||||||
return handlerErr
|
return handlerErr
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sIngressDeleteRequests
|
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)
|
log.Error().Err(err).Str("context", "deleteKubernetesIngresses").Msg("Unable to decode and validate the request payload")
|
||||||
|
return httperror.BadRequest("Unable to decode and validate the request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.DeleteIngresses(payload)
|
err = cli.DeleteIngresses(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "deleteKubernetesIngresses").Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "deleteKubernetesIngresses").Msg("Unable to retrieve ingresses from the Kubernetes for a namespace level user")
|
||||||
|
return httperror.NotFound("Unable to retrieve ingresses from the Kubernetes for a namespace level user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "deleteKubernetesIngresses").Msg("Unable to delete ingresses")
|
||||||
return httperror.InternalServerError("Unable to delete ingresses", err)
|
return httperror.InternalServerError("Unable to delete ingresses", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id updateKubernetesIngress
|
// @id UpdateKubernetesIngress
|
||||||
// @summary Update kubernetes ingress rule
|
// @summary Update an Ingress
|
||||||
// @description Update kubernetes ingress rule for the provided environment
|
// @description Update an Ingress for the provided environment.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace name"
|
// @param namespace path string true "Namespace name"
|
||||||
// @param body body models.K8sIngressInfo true "Ingress details"
|
// @param body body models.K8sIngressInfo true "Ingress details"
|
||||||
// @success 200 {string} string "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or unable to find the specified ingress."
|
||||||
|
// @failure 500 "Server error occurred while attempting to update the specified ingress."
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [put]
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [put]
|
||||||
func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
log.Error().Err(err).Str("context", "updateKubernetesIngress").Msg("Unable to retrieve namespace from request")
|
||||||
|
return httperror.BadRequest("Unable to retrieve namespace from request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sIngressInfo
|
payload := models.K8sIngressInfo{}
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
log.Error().Err(err).Str("context", "updateKubernetesIngress").Msg("Unable to decode and validate the request payload")
|
||||||
|
return httperror.BadRequest("Unable to decode and validate the request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
cli, handlerErr := handler.getProxyKubeClient(r)
|
||||||
|
@ -632,7 +817,18 @@ func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.R
|
||||||
|
|
||||||
err = cli.UpdateIngress(namespace, payload)
|
err = cli.UpdateIngress(namespace, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to update the ingress", err)
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "updateKubernetesIngress").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "updateKubernetesIngress").Str("namespace", namespace).Msg("Unable to retrieve ingresses from the K ubernetes for a namespace level user")
|
||||||
|
return httperror.NotFound("Unable to retrieve ingresses from the Kubernetes for a namespace level user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "updateKubernetesIngress").Str("namespace", namespace).Msg("Unable to update ingress in a namespace")
|
||||||
|
return httperror.InternalServerError("Unable to update ingress in a namespace", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
|
|
|
@ -7,23 +7,22 @@ import (
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id getKubernetesMetricsForAllNodes
|
// @id GetKubernetesMetricsForAllNodes
|
||||||
// @summary Get a list of nodes with their live metrics
|
// @summary Get a list of nodes with their live metrics
|
||||||
// @description Get a list of nodes with their live metrics
|
// @description Get a list of metrics associated with all nodes of a cluster.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @success 200 {object} v1beta1.NodeMetricsList "Success"
|
// @success 200 {object} v1beta1.NodeMetricsList "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
// @failure 500 "Server error"
|
// @failure 500 "Server error occurred while attempting to retrieve the list of nodes with their live metrics."
|
||||||
// @router /kubernetes/{id}/metrics/nodes [get]
|
// @router /kubernetes/{id}/metrics/nodes [get]
|
||||||
func (handler *Handler) getKubernetesMetricsForAllNodes(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesMetricsForAllNodes(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoint, err := middlewares.FetchEndpoint(r)
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
|
@ -33,45 +32,49 @@ func (handler *Handler) getKubernetesMetricsForAllNodes(w http.ResponseWriter, r
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllNodes").Msg("Failed to create metrics KubeClient")
|
||||||
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
|
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics, err := cli.MetricsV1beta1().NodeMetricses().List(r.Context(), v1.ListOptions{})
|
metrics, err := cli.MetricsV1beta1().NodeMetricses().List(r.Context(), v1.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllNodes").Msg("Failed to fetch metrics")
|
||||||
return httperror.InternalServerError("Failed to fetch metrics", err)
|
return httperror.InternalServerError("Failed to fetch metrics", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, metrics)
|
return response.JSON(w, metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id getKubernetesMetricsForNode
|
// @id GetKubernetesMetricsForNode
|
||||||
// @summary Get live metrics for a node
|
// @summary Get live metrics for a node
|
||||||
// @description Get live metrics for a node
|
// @description Get live metrics for the specified node.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param name path string true "Node identifier"
|
// @param name path string true "Node identifier"
|
||||||
// @success 200 {object} v1beta1.NodeMetrics "Success"
|
// @success 200 {object} v1beta1.NodeMetrics "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the live metrics for the specified node."
|
||||||
// @router /kubernetes/{id}/metrics/nodes/{name} [get]
|
// @router /kubernetes/{id}/metrics/nodes/{name} [get]
|
||||||
func (handler *Handler) getKubernetesMetricsForNode(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesMetricsForNode(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoint, err := middlewares.FetchEndpoint(r)
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Failed to fetch endpoint")
|
||||||
return httperror.InternalServerError(err.Error(), err)
|
return httperror.InternalServerError(err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Failed to create metrics KubeClient")
|
||||||
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
|
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeName, err := request.RetrieveRouteVariableValue(r, "name")
|
nodeName, err := request.RetrieveRouteVariableValue(r, "name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Invalid node identifier route variable")
|
||||||
return httperror.BadRequest("Invalid node identifier route variable", err)
|
return httperror.BadRequest("Invalid node identifier route variable", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,90 +84,98 @@ func (handler *Handler) getKubernetesMetricsForNode(w http.ResponseWriter, r *ht
|
||||||
v1.GetOptions{},
|
v1.GetOptions{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Failed to fetch metrics")
|
||||||
return httperror.InternalServerError("Failed to fetch metrics", err)
|
return httperror.InternalServerError("Failed to fetch metrics", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, metrics)
|
return response.JSON(w, metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id getKubernetesMetricsForAllPods
|
// @id GetKubernetesMetricsForAllPods
|
||||||
// @summary Get a list of pods with their live metrics
|
// @summary Get a list of pods with their live metrics
|
||||||
// @description Get a list of pods with their live metrics
|
// @description Get a list of pods with their live metrics for the specified namespace.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace"
|
// @param namespace path string true "Namespace"
|
||||||
// @success 200 {object} v1beta1.PodMetricsList "Success"
|
// @success 200 {object} v1beta1.PodMetricsList "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the list of pods with their live metrics."
|
||||||
// @router /kubernetes/{id}/metrics/pods/{namespace} [get]
|
// @router /kubernetes/{id}/metrics/pods/{namespace} [get]
|
||||||
func (handler *Handler) getKubernetesMetricsForAllPods(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesMetricsForAllPods(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoint, err := middlewares.FetchEndpoint(r)
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Failed to fetch endpoint")
|
||||||
return httperror.InternalServerError(err.Error(), err)
|
return httperror.InternalServerError(err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Failed to create metrics KubeClient")
|
||||||
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
|
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Invalid namespace identifier route variable")
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics, err := cli.MetricsV1beta1().PodMetricses(namespace).List(r.Context(), v1.ListOptions{})
|
metrics, err := cli.MetricsV1beta1().PodMetricses(namespace).List(r.Context(), v1.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Failed to fetch metrics")
|
||||||
return httperror.InternalServerError("Failed to fetch metrics", err)
|
return httperror.InternalServerError("Failed to fetch metrics", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, metrics)
|
return response.JSON(w, metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id getKubernetesMetricsForPod
|
// @id GetKubernetesMetricsForPod
|
||||||
// @summary Get live metrics for a pod
|
// @summary Get live metrics for a pod
|
||||||
// @description Get live metrics for a pod
|
// @description Get live metrics for the specified pod.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace"
|
// @param namespace path string true "Namespace"
|
||||||
// @param name path string true "Pod identifier"
|
// @param name path string true "Pod identifier"
|
||||||
// @success 200 {object} v1beta1.PodMetrics "Success"
|
// @success 200 {object} v1beta1.PodMetrics "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the live metrics for the specified pod."
|
||||||
// @router /kubernetes/{id}/metrics/pods/{namespace}/{name} [get]
|
// @router /kubernetes/{id}/metrics/pods/{namespace}/{name} [get]
|
||||||
func (handler *Handler) getKubernetesMetricsForPod(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesMetricsForPod(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoint, err := middlewares.FetchEndpoint(r)
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Msg("Failed to fetch endpoint")
|
||||||
return httperror.InternalServerError(err.Error(), err)
|
return httperror.InternalServerError(err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Msg("Failed to create metrics KubeClient")
|
||||||
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
|
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Msg("Invalid namespace identifier route variable")
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
podName, err := request.RetrieveRouteVariableValue(r, "name")
|
podName, err := request.RetrieveRouteVariableValue(r, "name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Msg("Invalid pod identifier route variable")
|
||||||
return httperror.BadRequest("Invalid pod identifier route variable", err)
|
return httperror.BadRequest("Invalid pod identifier route variable", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics, err := cli.MetricsV1beta1().PodMetricses(namespace).Get(r.Context(), podName, v1.GetOptions{})
|
metrics, err := cli.MetricsV1beta1().PodMetricses(namespace).Get(r.Context(), podName, v1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Str("namespace", namespace).Str("pod", podName).Msg("Failed to fetch metrics")
|
||||||
return httperror.InternalServerError("Failed to fetch metrics", err)
|
return httperror.InternalServerError("Failed to fetch metrics", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,185 +1,282 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id getKubernetesNamespaces
|
// @id GetKubernetesNamespaces
|
||||||
// @summary Get a list of kubernetes namespaces
|
// @summary Get a list of namespaces
|
||||||
// @description Get a list of all kubernetes namespaces in the cluster
|
// @description Get a list of all namespaces within the given environment based on the user role and permissions. If the user is an admin, they can access all namespaces. If the user is not an admin, they can only access namespaces that they have access to.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @success 200 {object} map[string]portainer.K8sNamespaceInfo "Success"
|
// @param withResourceQuota query boolean true "When set to true, include the resource quota information as part of the Namespace information. Default is false"
|
||||||
// @failure 400 "Invalid request"
|
// @success 200 {array} portainer.K8sNamespaceInfo "Success"
|
||||||
// @failure 500 "Server error"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the list of namespaces."
|
||||||
// @router /kubernetes/{id}/namespaces [get]
|
// @router /kubernetes/{id}/namespaces [get]
|
||||||
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, handlerErr := handler.getProxyKubeClient(r)
|
withResourceQuota, err := request.RetrieveBooleanQueryParameter(r, "withResourceQuota", true)
|
||||||
if handlerErr != nil {
|
if err != nil {
|
||||||
return handlerErr
|
log.Error().Err(err).Str("context", "GetKubernetesNamespaces").Msg("Invalid query parameter withResourceQuota")
|
||||||
|
return httperror.BadRequest("an error occurred during the GetKubernetesNamespaces operation, invalid query parameter withResourceQuota. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetKubernetesNamespaces").Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespaces operation, unable to get a Kubernetes client for the user. Error: ", httpErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
namespaces, err := cli.GetNamespaces()
|
namespaces, err := cli.GetNamespaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve namespaces", err)
|
log.Error().Err(err).Str("context", "GetKubernetesNamespaces").Msg("Unable to retrieve namespaces from the Kubernetes cluster")
|
||||||
|
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespaces operation, unable to retrieve namespaces from the Kubernetes cluster. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, namespaces)
|
if withResourceQuota {
|
||||||
|
return cli.CombineNamespacesWithResourceQuotas(namespaces, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, cli.ConvertNamespaceMapToSlice(namespaces))
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id getKubernetesNamespace
|
// @id GetKubernetesNamespacesCount
|
||||||
// @summary Get kubernetes namespace details
|
// @summary Get the total number of kubernetes namespaces within the given Portainer environment.
|
||||||
// @description Get kubernetes namespace details for the provided namespace within the given environment
|
// @description Get the total number of kubernetes namespaces within the given environment, including the system namespaces. The total count depends on the user's role and permissions.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace"
|
// @success 200 {integer} integer "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to compute the namespace count."
|
||||||
|
// @router /kubernetes/{id}/namespaces/count [get]
|
||||||
|
func (handler *Handler) getKubernetesNamespacesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetKubernetesNamespacesCount").Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespacesCount operation, unable to get a Kubernetes client for the user. Error: ", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaces, err := cli.GetNamespaces()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesNamespacesCount").Msg("Unable to retrieve namespaces from the Kubernetes cluster to count the total")
|
||||||
|
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespacesCount operation, unable to retrieve namespaces from the Kubernetes cluster to count the total. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, len(namespaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetKubernetesNamespace
|
||||||
|
// @summary Get namespace details
|
||||||
|
// @description Get namespace details for the provided namespace within the given environment.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param namespace path string true "The namespace name to get details for"
|
||||||
|
// @param withResourceQuota query boolean true "When set to true, include the resource quota information as part of the Namespace information. Default is false"
|
||||||
// @success 200 {object} portainer.K8sNamespaceInfo "Success"
|
// @success 200 {object} portainer.K8sNamespaceInfo "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific namespace."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve specified namespace information."
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace} [get]
|
// @router /kubernetes/{id}/namespaces/{namespace} [get]
|
||||||
func (handler *Handler) getKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
ns, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespaceName, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest(
|
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Invalid namespace parameter namespace")
|
||||||
"Invalid namespace identifier route variable",
|
return httperror.BadRequest("an error occurred during the GetKubernetesNamespace operation, invalid namespace parameter namespace. Error: ", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
withResourceQuota, err := request.RetrieveBooleanQueryParameter(r, "withResourceQuota", true)
|
||||||
if handlerErr != nil {
|
if err != nil {
|
||||||
return handlerErr
|
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Invalid query parameter withResourceQuota")
|
||||||
|
return httperror.BadRequest("an error occurred during the GetKubernetesNamespace operation for the namespace %s, invalid query parameter withResourceQuota. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace, err := cli.GetNamespace(ns)
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetKubernetesNamespace").Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespace operation for the namespace %s, unable to get a Kubernetes client for the user. Error: ", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaceInfo, err := cli.GetNamespace(namespaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve namespace", err)
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Unable to find the namespace")
|
||||||
|
return httperror.NotFound(fmt.Sprintf("an error occurred during the GetKubernetesNamespace operation for the namespace %s, unable to find the namespace. Error: ", namespaceName), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Unauthorized to access the namespace")
|
||||||
|
return httperror.Forbidden(fmt.Sprintf("an error occurred during the GetKubernetesNamespace operation, unauthorized to access the namespace: %s. Error: ", namespaceName), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Unable to get the namespace")
|
||||||
|
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the GetKubernetesNamespace operation, unable to get the namespace: %s. Error: ", namespaceName), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if withResourceQuota {
|
||||||
|
return cli.CombineNamespaceWithResourceQuota(namespaceInfo, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, namespaceInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id CreateKubernetesNamespace
|
||||||
|
// @summary Create a namespace
|
||||||
|
// @description Create a namespace within the given environment.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param body body models.K8sNamespaceDetails true "Namespace configuration details"
|
||||||
|
// @success 200 {object} portainer.K8sNamespaceInfo "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 409 "Conflict - the namespace already exists."
|
||||||
|
// @failure 500 "Server error occurred while attempting to create the namespace."
|
||||||
|
// @router /kubernetes/{id}/namespaces [post]
|
||||||
|
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
payload := models.K8sNamespaceDetails{}
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "CreateKubernetesNamespace").Msg("Invalid request payload")
|
||||||
|
return httperror.BadRequest("an error occurred during the CreateKubernetesNamespace operation, invalid request payload. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaceName := payload.Name
|
||||||
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "CreateKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("an error occurred during the CreateKubernetesNamespace operation for the namespace %s, unable to get a Kubernetes client for the user. Error: ", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, err := cli.CreateNamespace(payload)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsAlreadyExists(err) {
|
||||||
|
log.Error().Err(err).Str("context", "CreateKubernetesNamespace").Str("namespace", namespaceName).Msg("The namespace already exists")
|
||||||
|
return httperror.Conflict(fmt.Sprintf("an error occurred during the CreateKubernetesNamespace operation, the namespace %s already exists. Error: ", namespaceName), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "CreateKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to create the namespace")
|
||||||
|
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the CreateKubernetesNamespace operation, unable to create the namespace: %s", namespaceName), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, namespace)
|
return response.JSON(w, namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id createKubernetesNamespace
|
// @id DeleteKubernetesNamespace
|
||||||
// @summary Create a kubernetes namespace
|
// @summary Delete a kubernetes namespace
|
||||||
// @description Create a kubernetes namespace within the given environment
|
// @description Delete a kubernetes namespace within the given environment.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
// @param id path int true "Environment identifier"
|
||||||
// @accept json
|
|
||||||
// @produce json
|
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
|
||||||
// @param body body models.K8sNamespaceDetails true "Namespace configuration details"
|
|
||||||
// @success 200 {string} string "Success"
|
// @success 200 {string} string "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 403 "Unauthorized access or operation not allowed."
|
||||||
// @router /kubernetes/{id}/namespaces [post]
|
// @failure 500 "Server error occurred while attempting to delete the namespace."
|
||||||
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
// @router /kubernetes/{id}/namespaces [delete]
|
||||||
var payload models.K8sNamespaceDetails
|
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
|
||||||
if handlerErr != nil {
|
|
||||||
return handlerErr
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cli.CreateNamespace(payload)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to create namespace", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// @id deleteKubernetesNamespace
|
|
||||||
// @summary Delete kubernetes namespace
|
|
||||||
// @description Delete a kubernetes namespace within the given environment
|
|
||||||
// @description **Access policy**: authenticated
|
|
||||||
// @tags kubernetes
|
|
||||||
// @security ApiKeyAuth
|
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
|
||||||
// @param namespace path string true "Namespace"
|
|
||||||
// @success 200 {string} string "Success"
|
|
||||||
// @failure 400 "Invalid request"
|
|
||||||
// @failure 500 "Server error"
|
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace} [delete]
|
|
||||||
func (handler *Handler) deleteKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) deleteKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
var payload models.K8sNamespaceDetails
|
namespaceNames, err := request.GetPayload[deleteKubernetesNamespacePayload](r)
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
log.Error().Err(err).Str("context", "DeleteKubernetesNamespace").Msg("Invalid namespace identifier route variable")
|
||||||
|
return httperror.BadRequest("an error occurred during the DeleteKubernetesNamespace operation, invalid namespace identifier route variable. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
if err != nil {
|
if httpErr != nil {
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
log.Error().Err(httpErr).Str("context", "DeleteKubernetesNamespace").Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("an error occurred during the DeleteKubernetesNamespace operation for the namespace %s, unable to get a Kubernetes client for the user. Error: ", httpErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
for _, namespaceName := range *namespaceNames {
|
||||||
if handlerErr != nil {
|
_, err := cli.DeleteNamespace(namespaceName)
|
||||||
return handlerErr
|
if err != nil {
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "DeleteKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to find the namespace")
|
||||||
|
return httperror.NotFound(fmt.Sprintf("an error occurred during the DeleteKubernetesNamespace operation for the namespace %s, unable to find the namespace. Error: ", namespaceName), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.DeleteNamespace(namespace)
|
log.Error().Err(err).Str("context", "DeleteKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to delete the namespace")
|
||||||
if err != nil {
|
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the DeleteKubernetesNamespace operation for the namespace %s, unable to delete the Kubernetes namespace. Error: ", namespaceName), err)
|
||||||
return httperror.InternalServerError("Unable to delete namespace", err)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, namespaceNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
type deleteKubernetesNamespacePayload []string
|
||||||
|
|
||||||
|
func (payload deleteKubernetesNamespacePayload) Validate(r *http.Request) error {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return errors.New("namespace names are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id updateKubernetesNamespace
|
// @id UpdateKubernetesNamespace
|
||||||
// @summary Updates a kubernetes namespace
|
// @summary Update a namespace
|
||||||
// @description Update a kubernetes namespace within the given environment
|
// @description Update a namespace within the given environment.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace"
|
// @param namespace path string true "Namespace"
|
||||||
// @param body body models.K8sNamespaceDetails true "Namespace details"
|
// @param body body models.K8sNamespaceDetails true "Namespace details"
|
||||||
// @success 200 {string} string "Success"
|
// @success 200 {object} portainer.K8sNamespaceInfo "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific namespace."
|
||||||
|
// @failure 500 "Server error occurred while attempting to update the namespace."
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace} [put]
|
// @router /kubernetes/{id}/namespaces/{namespace} [put]
|
||||||
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
var payload models.K8sNamespaceDetails
|
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("an error occurred during the UpdateKubernetesNamespace operation, invalid request payload. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
namespaceName := payload.Name
|
||||||
if handlerErr != nil {
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
return handlerErr
|
if httpErr != nil {
|
||||||
|
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the UpdateKubernetesNamespace operation for the namespace %s, unable to get a Kubernetes client for the user. Error: ", namespaceName), httpErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.UpdateNamespace(payload)
|
namespace, err := cli.UpdateNamespace(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to update namespace", err)
|
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the UpdateKubernetesNamespace operation for the namespace %s, unable to update the Kubernetes namespace. Error: ", namespaceName), err)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return response.JSON(w, namespace)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,53 +6,72 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/middlewares"
|
"github.com/portainer/portainer/api/http/middlewares"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id GetKubernetesNodesLimits
|
// @id GetKubernetesNodesLimits
|
||||||
// @summary Get CPU and memory limits of all nodes within k8s cluster
|
// @summary Get CPU and memory limits of all nodes within k8s cluster
|
||||||
// @description Get CPU and memory limits of all nodes within k8s cluster
|
// @description Get CPU and memory limits of all nodes within k8s cluster.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment(Endpoint) identifier"
|
// @param id path int true "Environment(Endpoint) identifier"
|
||||||
// @success 200 {object} portainer.K8sNodesLimits "Success"
|
// @success 200 {object} portainer.K8sNodesLimits "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request"
|
||||||
// @failure 401 "Unauthorized"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
// @failure 403 "Permission denied"
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
// @failure 404 "Environment(Endpoint) not found"
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
// @failure 500 "Server error"
|
// @failure 500 "Server error occurred while attempting to retrieve nodes limits."
|
||||||
// @router /kubernetes/{id}/nodes_limits [get]
|
// @router /kubernetes/{id}/nodes_limits [get]
|
||||||
func (handler *Handler) getKubernetesNodesLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesNodesLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoint, err := middlewares.FetchEndpoint(r)
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesNodesLimits").Msg("Unable to find an environment on request context")
|
||||||
return httperror.NotFound("Unable to find an environment on request context", err)
|
return httperror.NotFound("Unable to find an environment on request context", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesNodesLimits").Msg("Unable to create Kubernetes client")
|
||||||
return httperror.InternalServerError("Unable to create Kubernetes client", err)
|
return httperror.InternalServerError("Unable to create Kubernetes client", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
nodesLimits, err := cli.GetNodesLimits()
|
nodesLimits, err := cli.GetNodesLimits()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesNodesLimits").Msg("Unable to retrieve nodes limits")
|
||||||
return httperror.InternalServerError("Unable to retrieve nodes limits", err)
|
return httperror.InternalServerError("Unable to retrieve nodes limits", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, nodesLimits)
|
return response.JSON(w, nodesLimits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @id GetKubernetesMaxResourceLimits
|
||||||
|
// @summary Get max CPU and memory limits of all nodes within k8s cluster
|
||||||
|
// @description Get max CPU and memory limits (unused resources) of all nodes within k8s cluster.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment(Endpoint) identifier"
|
||||||
|
// @success 200 {object} portainer.K8sNodesLimits "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve nodes limits."
|
||||||
|
// @router /kubernetes/{id}/max_resource_limits [get]
|
||||||
func (handler *Handler) getKubernetesMaxResourceLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesMaxResourceLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoint, err := middlewares.FetchEndpoint(r)
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesMaxResourceLimits").Msg("Unable to find an environment on request context")
|
||||||
return httperror.NotFound("Unable to find an environment on request context", err)
|
return httperror.NotFound("Unable to find an environment on request context", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Failed to lookup KubeClient", err)
|
log.Error().Err(err).Str("context", "GetKubernetesMaxResourceLimits").Msg("Unable to create Kubernetes client")
|
||||||
|
return httperror.InternalServerError("Unable to create Kubernetes client", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
overCommit := endpoint.Kubernetes.Configuration.EnableResourceOverCommit
|
overCommit := endpoint.Kubernetes.Configuration.EnableResourceOverCommit
|
||||||
|
@ -61,6 +80,7 @@ func (handler *Handler) getKubernetesMaxResourceLimits(w http.ResponseWriter, r
|
||||||
// name is set to "" so all namespaces resources are considered when calculating max resource limits
|
// name is set to "" so all namespaces resources are considered when calculating max resource limits
|
||||||
resourceLimit, err := cli.GetMaxResourceLimits("", overCommit, overCommitPercent)
|
resourceLimit, err := cli.GetMaxResourceLimits("", overCommit, overCommitPercent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesMaxResourceLimits").Msg("Unable to retrieve max resource limit")
|
||||||
return httperror.InternalServerError("Unable to retrieve max resource limit", err)
|
return httperror.InternalServerError("Unable to retrieve max resource limit", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,28 +5,33 @@ import (
|
||||||
|
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id IsRBACEnabled
|
// @id GetKubernetesRBACStatus
|
||||||
// @summary Check if RBAC is enabled
|
// @summary Check if RBAC is enabled
|
||||||
// @description Check if RBAC is enabled in the current Kubernetes cluster.
|
// @description Check if RBAC is enabled in the specified Kubernetes cluster.
|
||||||
// @description **Access policy**: administrator
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags rbac_enabled
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
// @produce json
|
||||||
// @produce text/plain
|
|
||||||
// @param id path int true "Environment(Endpoint) identifier"
|
// @param id path int true "Environment(Endpoint) identifier"
|
||||||
// @success 200 "Success"
|
// @success 200 {boolean} bool "RBAC status"
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the RBAC status."
|
||||||
// @router /kubernetes/{id}/rbac_enabled [get]
|
// @router /kubernetes/{id}/rbac_enabled [get]
|
||||||
func (handler *Handler) isRBACEnabled(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesRBACStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
cli, handlerErr := handler.getProxyKubeClient(r)
|
||||||
if handlerErr != nil {
|
if handlerErr != nil {
|
||||||
return handlerErr
|
log.Error().Err(handlerErr).Str("context", "GetKubernetesRBACStatus").Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("Unable to get a Kubernetes client for the user. Error: ", handlerErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
isRBACEnabled, err := cli.IsRBACEnabled()
|
isRBACEnabled, err := cli.IsRBACEnabled()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesRBACStatus").Msg("Failed to check RBAC status")
|
||||||
return httperror.InternalServerError("Failed to check RBAC status", err)
|
return httperror.InternalServerError("Failed to check RBAC status", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
40
api/http/handler/kubernetes/role_bindings.go
Normal file
40
api/http/handler/kubernetes/role_bindings.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetKubernetesRoleBindings
|
||||||
|
// @summary Get a list of kubernetes role bindings
|
||||||
|
// @description Get a list of kubernetes role bindings that the user has access to.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {array} kubernetes.K8sRoleBinding "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the list of role bindings."
|
||||||
|
// @router /kubernetes/{id}/rolebindings [get]
|
||||||
|
func (handler *Handler) getAllKubernetesRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetAllKubernetesRoleBindings").Msg("Unable to prepare kube client")
|
||||||
|
return httperror.InternalServerError("unable to prepare kube client. Error: ", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
rolebindings, err := cli.GetRoleBindings("")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesRoleBindings").Msg("Unable to fetch rolebindings")
|
||||||
|
return httperror.InternalServerError("unable to fetch rolebindings. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, rolebindings)
|
||||||
|
}
|
40
api/http/handler/kubernetes/roles.go
Normal file
40
api/http/handler/kubernetes/roles.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetKubernetesRoles
|
||||||
|
// @summary Get a list of kubernetes roles
|
||||||
|
// @description Get a list of kubernetes roles that the user has access to.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {array} kubernetes.K8sRole "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the list of roles."
|
||||||
|
// @router /kubernetes/{id}/roles [get]
|
||||||
|
func (handler *Handler) getAllKubernetesRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetAllKubernetesRoles").Msg("Unable to prepare kube client")
|
||||||
|
return httperror.InternalServerError("unable to prepare kube client. Error: ", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := cli.GetRoles("")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesRoles").Msg("Unable to fetch roles across all namespaces")
|
||||||
|
return httperror.InternalServerError("unable to fetch roles across all namespaces. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, roles)
|
||||||
|
}
|
143
api/http/handler/kubernetes/secrets.go
Normal file
143
api/http/handler/kubernetes/secrets.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetKubernetesSecret
|
||||||
|
// @summary Get a Secret
|
||||||
|
// @description Get a Secret by name for a given namespace.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param namespace path string true "The namespace name where the secret is located"
|
||||||
|
// @param secret path string true "The secret name to get details for"
|
||||||
|
// @success 200 {object} models.K8sSecret "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve a secret by name belong in a namespace."
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/secrets/{secret} [get]
|
||||||
|
func (handler *Handler) getKubernetesSecret(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
|
||||||
|
return httperror.BadRequest("unable to retrieve namespace identifier route variable. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretName, err := request.RetrieveRouteVariableValue(r, "secret")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Msg("Unable to retrieve secret identifier route variable")
|
||||||
|
return httperror.BadRequest("unable to retrieve secret identifier route variable. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("unable to get a Kubernetes client for the user. Error: ", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := cli.GetSecret(namespace, secretName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Str("secret", secretName).Msg("Unable to get secret")
|
||||||
|
return httperror.InternalServerError("unable to get secret. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretWithApplication, err := cli.CombineSecretWithApplications(secret)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Str("secret", secretName).Msg("Unable to combine secret with associated applications")
|
||||||
|
return httperror.InternalServerError("unable to combine secret with associated applications. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, secretWithApplication)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetKubernetesSecrets
|
||||||
|
// @summary Get a list of Secrets
|
||||||
|
// @description Get a list of Secrets for a given namespace. If isUsed is set to true, information about the applications that use the secrets is also returned.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param isUsed query bool true "When set to true, associate the Secrets with the applications that use them"
|
||||||
|
// @success 200 {array} models.K8sSecret "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve all secrets from the cluster."
|
||||||
|
// @router /kubernetes/{id}/secrets [get]
|
||||||
|
func (handler *Handler) GetAllKubernetesSecrets(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
secrets, err := handler.getAllKubernetesSecrets(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, secrets)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetKubernetesSecretsCount
|
||||||
|
// @summary Get Secrets count
|
||||||
|
// @description Get the count of Secrets across all namespaces that the user has access to.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {integer} integer "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the count of all secrets from the cluster."
|
||||||
|
// @router /kubernetes/{id}/secrets/count [get]
|
||||||
|
func (handler *Handler) getAllKubernetesSecretsCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
secrets, err := handler.getAllKubernetesSecrets(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, len(secrets))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getAllKubernetesSecrets(r *http.Request) ([]models.K8sSecret, *httperror.HandlerError) {
|
||||||
|
isUsed, err := request.RetrieveBooleanQueryParameter(r, "isUsed", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesSecrets").Msg("Unable to retrieve isUsed query parameter")
|
||||||
|
return nil, httperror.BadRequest("unable to retrieve isUsed query parameter. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetAllKubernetesSecrets").Msg("Unable to prepare kube client")
|
||||||
|
return nil, httperror.InternalServerError("unable to prepare kube client. Error: ", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := cli.GetSecrets("")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesSecrets").Msg("Unable to get secrets")
|
||||||
|
return nil, httperror.InternalServerError("unable to get secrets. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isUsed {
|
||||||
|
secretsWithApplications, err := cli.CombineSecretsWithApplications(secrets)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesSecrets").Msg("Unable to combine secrets with associated applications")
|
||||||
|
return nil, httperror.InternalServerError("unable to combine secrets with associated applications. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretsWithApplications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets, nil
|
||||||
|
}
|
40
api/http/handler/kubernetes/service_accounts.go
Normal file
40
api/http/handler/kubernetes/service_accounts.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetKubernetesServiceAccounts
|
||||||
|
// @summary Get a list of kubernetes service accounts
|
||||||
|
// @description Get a list of kubernetes service accounts that the user has access to.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {array} kubernetes.K8sServiceAccount "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the list of service accounts."
|
||||||
|
// @router /kubernetes/{id}/serviceaccounts [get]
|
||||||
|
func (handler *Handler) getAllKubernetesServiceAccounts(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetAllKubernetesServiceAccounts").Msg("Unable to prepare kube client")
|
||||||
|
return httperror.InternalServerError("unable to prepare kube client. Error: ", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceAccounts, err := cli.GetServiceAccounts("")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesServiceAccounts").Msg("Unable to fetch service accounts across all namespaces")
|
||||||
|
return httperror.InternalServerError("unable to fetch service accounts. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, serviceAccounts)
|
||||||
|
}
|
|
@ -7,165 +7,298 @@ import (
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id getKubernetesServices
|
// @id GetKubernetesServices
|
||||||
// @summary Get a list of kubernetes services for a given namespace
|
// @summary Get a list of services
|
||||||
// @description Get a list of kubernetes services for a given namespace
|
// @description Get a list of services that the user has access to.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace name"
|
// @param withApplications query boolean false "Lookup applications associated with each service"
|
||||||
// @param lookupapplications query boolean false "Lookup applications associated with each service"
|
|
||||||
// @success 200 {array} models.K8sServiceInfo "Success"
|
// @success 200 {array} models.K8sServiceInfo "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/services [get]
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
// @failure 500 "Server error occurred while attempting to retrieve all services."
|
||||||
|
// @router /kubernetes/{id}/services [get]
|
||||||
|
func (handler *Handler) GetAllKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
services, err := handler.getAllKubernetesServices(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
|
||||||
if handlerErr != nil {
|
|
||||||
return handlerErr
|
|
||||||
}
|
|
||||||
|
|
||||||
lookup, err := request.RetrieveBooleanQueryParameter(r, "lookupapplications", true)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.BadRequest("Invalid lookupapplications query parameter", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
services, err := cli.GetServices(namespace, lookup)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to retrieve services", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, services)
|
return response.JSON(w, services)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id createKubernetesService
|
// @id GetAllKubernetesServicesCount
|
||||||
// @summary Create a kubernetes service
|
// @summary Get services count
|
||||||
// @description Create a kubernetes service within a given namespace
|
// @description Get the count of services that the user has access to.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {integer} integer "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve the total count of all services."
|
||||||
|
// @router /kubernetes/{id}/services/count [get]
|
||||||
|
func (handler *Handler) getAllKubernetesServicesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
services, err := handler.getAllKubernetesServices(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, len(services))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getAllKubernetesServices(r *http.Request) ([]models.K8sServiceInfo, *httperror.HandlerError) {
|
||||||
|
withApplications, err := request.RetrieveBooleanQueryParameter(r, "withApplications", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesServices").Msg("Unable to retrieve withApplications identifier")
|
||||||
|
return nil, httperror.BadRequest("unable to retrieve withApplications query parameter. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetAllKubernetesServices").Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return nil, httperror.InternalServerError("unable to get a Kubernetes client for the user. Error: ", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := cli.GetServices("")
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesServices").Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return nil, httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesServices").Msg("Unable to retrieve services from the Kubernetes for a cluster level user")
|
||||||
|
return nil, httperror.InternalServerError("unable to retrieve services from the Kubernetes for a cluster level user. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if withApplications && len(services) > 0 {
|
||||||
|
servicesWithApplications, err := cli.CombineServicesWithApplications(services)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetAllKubernetesServices").Msg("Unable to combine services with applications")
|
||||||
|
return nil, httperror.InternalServerError("unable to combine services with applications. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return servicesWithApplications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetKubernetesServicesByNamespace
|
||||||
|
// @summary Get a list of services for a given namespace
|
||||||
|
// @description Get a list of services for a given namespace.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param namespace path string true "Namespace name"
|
||||||
|
// @success 200 {array} models.K8sServiceInfo "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve all services for a namespace."
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/services [get]
|
||||||
|
func (handler *Handler) getKubernetesServicesByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesServicesByNamespace").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
|
||||||
|
return httperror.BadRequest("unable to retrieve namespace identifier route variable. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpError := handler.getProxyKubeClient(r)
|
||||||
|
if httpError != nil {
|
||||||
|
return httpError
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := cli.GetServices(namespace)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesServicesByNamespace").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesServicesByNamespace").Str("namespace", namespace).Msg("Unable to retrieve services from the Kubernetes for a namespace level user")
|
||||||
|
return httperror.InternalServerError("unable to retrieve services from the Kubernetes for a namespace level user. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, services)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id CreateKubernetesService
|
||||||
|
// @summary Create a service
|
||||||
|
// @description Create a service within a given namespace
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace name"
|
// @param namespace path string true "Namespace name"
|
||||||
// @param body body models.K8sServiceInfo true "Service definition"
|
// @param body body models.K8sServiceInfo true "Service definition"
|
||||||
// @success 200 "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier."
|
||||||
|
// @failure 500 "Server error occurred while attempting to create a service."
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/services [post]
|
// @router /kubernetes/{id}/namespaces/{namespace}/services [post]
|
||||||
func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
|
||||||
|
return httperror.BadRequest("unable to retrieve namespace identifier route variable. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sServiceInfo
|
var payload models.K8sServiceInfo
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Msg("Unable to decode and validate the request payload")
|
||||||
|
return httperror.BadRequest("unable to decode and validate the request payload. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
serviceName := payload.Name
|
||||||
if handlerErr != nil {
|
cli, httpError := handler.getProxyKubeClient(r)
|
||||||
return handlerErr
|
if httpError != nil {
|
||||||
|
log.Error().Err(httpError).Str("context", "CreateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("unable to get a Kubernetes client for the user. Error: ", httpError)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.CreateService(namespace, payload)
|
err = cli.CreateService(namespace, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to create sercice", err)
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if k8serrors.IsAlreadyExists(err) {
|
||||||
|
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("A service with the same name already exists in the namespace")
|
||||||
|
return httperror.Conflict("a service with the same name already exists in the namespace. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to create a service")
|
||||||
|
return httperror.InternalServerError("unable to create a service. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id deleteKubernetesServices
|
// @id DeleteKubernetesServices
|
||||||
// @summary Delete kubernetes services
|
// @summary Delete services
|
||||||
// @description Delete the provided list of kubernetes services
|
// @description Delete the provided list of services.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param body body models.K8sServiceDeleteRequests true "A map where the key is the namespace and the value is an array of services to delete"
|
// @param body body models.K8sServiceDeleteRequests true "A map where the key is the namespace and the value is an array of services to delete"
|
||||||
// @success 200 "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific service."
|
||||||
|
// @failure 500 "Server error occurred while attempting to delete services."
|
||||||
// @router /kubernetes/{id}/services/delete [post]
|
// @router /kubernetes/{id}/services/delete [post]
|
||||||
func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
var payload models.K8sServiceDeleteRequests
|
payload := models.K8sServiceDeleteRequests{}
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest(
|
log.Error().Err(err).Str("context", "DeleteKubernetesServices").Msg("Unable to decode and validate the request payload")
|
||||||
"Invalid request payload",
|
return httperror.BadRequest("unable to decode and validate the request payload. Error: ", err)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
cli, httpError := handler.getProxyKubeClient(r)
|
||||||
if handlerErr != nil {
|
if httpError != nil {
|
||||||
return handlerErr
|
return httpError
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.DeleteServices(payload)
|
err = cli.DeleteServices(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
"Unable to delete service",
|
log.Error().Err(err).Str("context", "DeleteKubernetesServices").Msg("Unauthorized access to the Kubernetes API")
|
||||||
err,
|
return httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "DeleteKubernetesServices").Msg("Unable to find the services to delete")
|
||||||
|
return httperror.NotFound("unable to find the services to delete. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "DeleteKubernetesServices").Msg("Unable to delete services")
|
||||||
|
return httperror.InternalServerError("unable to delete services. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id updateKubernetesService
|
// @id UpdateKubernetesService
|
||||||
// @summary Update a kubernetes service
|
// @summary Update a service
|
||||||
// @description Update a kubernetes service within a given namespace
|
// @description Update a service within a given namespace.
|
||||||
// @description **Access policy**: authenticated
|
// @description **Access policy**: Authenticated user.
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Environment (Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace name"
|
// @param namespace path string true "Namespace name"
|
||||||
// @param body body models.K8sServiceInfo true "Service definition"
|
// @param body body models.K8sServiceInfo true "Service definition"
|
||||||
// @success 200 "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 500 "Server error"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or unable to find the service to update."
|
||||||
|
// @failure 500 "Server error occurred while attempting to update a service."
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/services [put]
|
// @router /kubernetes/{id}/namespaces/{namespace}/services [put]
|
||||||
func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid namespace identifier route variable", err)
|
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
|
||||||
|
return httperror.BadRequest("unable to retrieve namespace identifier route variable. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sServiceInfo
|
var payload models.K8sServiceInfo
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Msg("Unable to decode and validate the request payload")
|
||||||
|
return httperror.BadRequest("unable to decode and validate the request payload. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, handlerErr := handler.getProxyKubeClient(r)
|
serviceName := payload.Name
|
||||||
if handlerErr != nil {
|
cli, httpError := handler.getProxyKubeClient(r)
|
||||||
return handlerErr
|
if httpError != nil {
|
||||||
|
log.Error().Err(httpError).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("unable to get a Kubernetes client for the user. Error: ", httpError)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.UpdateService(namespace, payload)
|
err = cli.UpdateService(namespace, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to update service", err)
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to find the service to update")
|
||||||
|
return httperror.NotFound("unable to find the service to update. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to update a service")
|
||||||
|
return httperror.InternalServerError("unable to update a service. Error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -21,18 +21,19 @@ func (payload *namespacesToggleSystemPayload) Validate(r *http.Request) error {
|
||||||
// @id KubernetesNamespacesToggleSystem
|
// @id KubernetesNamespacesToggleSystem
|
||||||
// @summary Toggle the system state for a namespace
|
// @summary Toggle the system state for a namespace
|
||||||
// @description Toggle the system state for a namespace
|
// @description Toggle the system state for a namespace
|
||||||
// @description **Access policy**: administrator or environment(endpoint) admin
|
// @description **Access policy**: Administrator or environment administrator.
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth || jwt
|
||||||
// @security jwt
|
|
||||||
// @tags kubernetes
|
// @tags kubernetes
|
||||||
// @accept json
|
// @accept json
|
||||||
// @param id path int true "Environment(Endpoint) identifier"
|
// @param id path int true "Environment identifier"
|
||||||
// @param namespace path string true "Namespace name"
|
// @param namespace path string true "Namespace name"
|
||||||
// @param body body namespacesToggleSystemPayload true "Update details"
|
// @param body body namespacesToggleSystemPayload true "Update details"
|
||||||
// @success 200 "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
// @failure 404 "Environment(Endpoint) not found"
|
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
|
||||||
// @failure 500 "Server error"
|
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
|
||||||
|
// @failure 404 "Unable to find an environment with the specified identifier or unable to find the namespace to update."
|
||||||
|
// @failure 500 "Server error occurred while attempting to update the system state of the namespace."
|
||||||
// @router /kubernetes/{id}/namespaces/{namespace}/system [put]
|
// @router /kubernetes/{id}/namespaces/{namespace}/system [put]
|
||||||
func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoint, err := middlewares.FetchEndpoint(r)
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
|
@ -51,7 +52,7 @@ func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.R
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
kubeClient, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
kubeClient, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to create kubernetes client", err)
|
return httperror.InternalServerError("Unable to create kubernetes client", err)
|
||||||
}
|
}
|
||||||
|
|
147
api/http/handler/kubernetes/volumes.go
Normal file
147
api/http/handler/kubernetes/volumes.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetAllKubernetesVolumes
|
||||||
|
// @summary Get Kubernetes volumes within the given Portainer environment
|
||||||
|
// @description Get a list of all kubernetes volumes within the given environment (Endpoint). The Endpoint ID must be a valid Portainer environment identifier.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param withApplications query boolean false "When set to True, include the applications that are using the volumes. It is set to false by default"
|
||||||
|
// @success 200 {object} map[string]kubernetes.K8sVolumeInfo "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 403 "Unauthorized access or operation not allowed."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve kubernetes volumes."
|
||||||
|
// @router /kubernetes/{id}/volumes [get]
|
||||||
|
func (handler *Handler) GetAllKubernetesVolumes(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
volumes, err := handler.getKubernetesVolumes(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, volumes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id getAllKubernetesVolumesCount
|
||||||
|
// @summary Get the total number of kubernetes volumes within the given Portainer environment.
|
||||||
|
// @description Get the total number of kubernetes volumes within the given environment (Endpoint). The total count depends on the user's role and permissions. The Endpoint ID must be a valid Portainer environment identifier.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @success 200 {integer} integer "Success"
|
||||||
|
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||||
|
// @failure 403 "Unauthorized access or operation not allowed."
|
||||||
|
// @failure 500 "Server error occurred while attempting to retrieve kubernetes volumes count."
|
||||||
|
// @router /kubernetes/{id}/volumes/count [get]
|
||||||
|
func (handler *Handler) getAllKubernetesVolumesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
volumes, err := handler.getKubernetesVolumes(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, len(volumes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetKubernetesVolume
|
||||||
|
// @summary Get a Kubernetes volume within the given Portainer environment
|
||||||
|
// @description Get a Kubernetes volume within the given environment (Endpoint). The Endpoint ID must be a valid Portainer environment identifier.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param namespace path string true "Namespace identifier"
|
||||||
|
// @param volume path string true "Volume name"
|
||||||
|
// @success 200 {object} kubernetes.K8sVolumeInfo "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/volumes/{namespace}/{volume} [get]
|
||||||
|
func (handler *Handler) getKubernetesVolume(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesVolume").Msg("Unable to retrieve namespace identifier")
|
||||||
|
return httperror.BadRequest("Invalid namespace identifier", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeName, err := request.RetrieveRouteVariableValue(r, "volume")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesVolume").Msg("Unable to retrieve volume name")
|
||||||
|
return httperror.BadRequest("Invalid volume name", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetKubernetesVolume").Msg("Unable to get Kubernetes client")
|
||||||
|
return httperror.InternalServerError("Failed to prepare Kubernetes client", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
volume, err := cli.GetVolume(namespace, volumeName)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesVolume").Str("namespace", namespace).Str("volume", volumeName).Msg("Unauthorized access")
|
||||||
|
return httperror.Unauthorized("Unauthorized access to volume", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesVolume").Str("namespace", namespace).Str("volume", volumeName).Msg("Volume not found")
|
||||||
|
return httperror.NotFound("Volume not found", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesVolume").Str("namespace", namespace).Str("volume", volumeName).Msg("Failed to retrieve volume")
|
||||||
|
return httperror.InternalServerError("Failed to retrieve volume", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getKubernetesVolumes(r *http.Request) ([]models.K8sVolumeInfo, *httperror.HandlerError) {
|
||||||
|
withApplications, err := request.RetrieveBooleanQueryParameter(r, "withApplications", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Bool("withApplications", withApplications).Msg("Unable to parse query parameter")
|
||||||
|
return nil, httperror.BadRequest("Invalid 'withApplications' parameter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.prepareKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "GetKubernetesVolumes").Msg("Unable to get Kubernetes client")
|
||||||
|
return nil, httperror.InternalServerError("Failed to prepare Kubernetes client", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, err := cli.GetVolumes("")
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Msg("Unauthorized access")
|
||||||
|
return nil, httperror.Unauthorized("Unauthorized access to volumes", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Msg("Failed to retrieve volumes")
|
||||||
|
return nil, httperror.InternalServerError("Failed to retrieve volumes", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if withApplications {
|
||||||
|
volumesWithApplications, err := cli.CombineVolumesWithApplications(&volumes)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Msg("Failed to combine volumes with applications")
|
||||||
|
return nil, httperror.InternalServerError("Failed to combine volumes with applications", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *volumesWithApplications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return volumes, nil
|
||||||
|
}
|
|
@ -135,7 +135,7 @@ func (handler *Handler) userHasRegistryAccess(r *http.Request, registry *portain
|
||||||
|
|
||||||
// validate access for kubernetes namespaces (leverage registry.RegistryAccesses[endpointId].Namespaces)
|
// validate access for kubernetes namespaces (leverage registry.RegistryAccesses[endpointId].Namespaces)
|
||||||
if endpointutils.IsKubernetesEndpoint(endpoint) {
|
if endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||||
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, errors.Wrap(err, "unable to retrieve kubernetes client to validate registry access")
|
return false, false, errors.Wrap(err, "unable to retrieve kubernetes client to validate registry access")
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
|
||||||
if useAuthentication {
|
if useAuthentication {
|
||||||
username, err := request.RetrieveMultiPartFormValue(r, "Username", false)
|
username, err := request.RetrieveMultiPartFormValue(r, "Username", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Invalid username")
|
return errors.New("invalid username")
|
||||||
}
|
}
|
||||||
payload.Username = username
|
payload.Username = username
|
||||||
|
|
||||||
|
@ -61,19 +61,19 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
|
||||||
if useTLS && !skipTLSVerify {
|
if useTLS && !skipTLSVerify {
|
||||||
cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile")
|
cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Invalid certificate file. Ensure that the file is uploaded correctly")
|
return errors.New("invalid certificate file. Ensure that the file is uploaded correctly")
|
||||||
}
|
}
|
||||||
payload.TLSCertFile = cert
|
payload.TLSCertFile = cert
|
||||||
|
|
||||||
key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile")
|
key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Invalid key file. Ensure that the file is uploaded correctly")
|
return errors.New("invalid key file. Ensure that the file is uploaded correctly")
|
||||||
}
|
}
|
||||||
payload.TLSKeyFile = key
|
payload.TLSKeyFile = key
|
||||||
|
|
||||||
ca, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile")
|
ca, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Invalid CA certificate file. Ensure that the file is uploaded correctly")
|
return errors.New("invalid CA certificate file. Ensure that the file is uploaded correctly")
|
||||||
}
|
}
|
||||||
payload.TLSCACertFile = ca
|
payload.TLSCACertFile = ca
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,15 +45,15 @@ type registryCreatePayload struct {
|
||||||
|
|
||||||
func (payload *registryCreatePayload) Validate(_ *http.Request) error {
|
func (payload *registryCreatePayload) Validate(_ *http.Request) error {
|
||||||
if len(payload.Name) == 0 {
|
if len(payload.Name) == 0 {
|
||||||
return errors.New("Invalid registry name")
|
return errors.New("invalid registry name")
|
||||||
}
|
}
|
||||||
if len(payload.URL) == 0 {
|
if len(payload.URL) == 0 {
|
||||||
return errors.New("Invalid registry URL")
|
return errors.New("invalid registry URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Authentication {
|
if payload.Authentication {
|
||||||
if len(payload.Username) == 0 || len(payload.Password) == 0 {
|
if len(payload.Username) == 0 || len(payload.Password) == 0 {
|
||||||
return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled")
|
return errors.New("invalid credentials. Username and password must be specified when authentication is enabled")
|
||||||
}
|
}
|
||||||
if payload.Type == portainer.EcrRegistry {
|
if payload.Type == portainer.EcrRegistry {
|
||||||
if len(payload.Ecr.Region) == 0 {
|
if len(payload.Ecr.Region) == 0 {
|
||||||
|
@ -127,10 +127,10 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
||||||
}
|
}
|
||||||
for _, r := range registries {
|
for _, r := range registries {
|
||||||
if r.Name == registry.Name {
|
if r.Name == registry.Name {
|
||||||
return httperror.Conflict("Another registry with the same name already exists", errors.New("A registry is already defined with this name"))
|
return httperror.Conflict("Another registry with the same name already exists", errors.New("a registry is already defined with this name"))
|
||||||
}
|
}
|
||||||
if handler.registriesHaveSameURLAndCredentials(&r, registry) {
|
if handler.registriesHaveSameURLAndCredentials(&r, registry) {
|
||||||
return httperror.Conflict("Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials"))
|
return httperror.Conflict("Another registry with the same URL and credentials already exists", errors.New("a registry is already defined for this URL and credentials"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ func (handler *Handler) deleteKubernetesSecrets(registry *portainer.Registry) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Skip environments that can't get a kubeclient from
|
// Skip environments that can't get a kubeclient from
|
||||||
log.Warn().Err(err).Msgf("Unable to get kubernetes client for environment %d", endpointId)
|
log.Warn().Err(err).Msgf("Unable to get kubernetes client for environment %d", endpointId)
|
||||||
|
|
|
@ -96,7 +96,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
// See https://portainer.atlassian.net/browse/EE-2706 for more details
|
// See https://portainer.atlassian.net/browse/EE-2706 for more details
|
||||||
for _, r := range registries {
|
for _, r := range registries {
|
||||||
if r.ID != registry.ID && r.Name == registry.Name {
|
if r.ID != registry.ID && r.Name == registry.Name {
|
||||||
return httperror.Conflict("Another registry with the same name already exists", errors.New("A registry is already defined with this name"))
|
return httperror.Conflict("Another registry with the same name already exists", errors.New("a registry is already defined with this name"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
|
|
||||||
for _, r := range registries {
|
for _, r := range registries {
|
||||||
if r.ID != registry.ID && handler.registriesHaveSameURLAndCredentials(&r, registry) {
|
if r.ID != registry.ID && handler.registriesHaveSameURLAndCredentials(&r, registry) {
|
||||||
return httperror.Conflict("Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials"))
|
return httperror.Conflict("Another registry with the same URL and credentials already exists", errors.New("a registry is already defined for this URL and credentials"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,7 +193,7 @@ func syncConfig(registry *portainer.Registry) *portainer.RegistryManagementConfi
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) updateEndpointRegistryAccess(endpoint *portainer.Endpoint, registry *portainer.Registry, endpointAccess portainer.RegistryAccessPolicies) error {
|
func (handler *Handler) updateEndpointRegistryAccess(endpoint *portainer.Endpoint, registry *portainer.Registry, endpointAccess portainer.RegistryAccessPolicies) error {
|
||||||
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,7 +163,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||||
// Refresh ECR registry secret if needed
|
// Refresh ECR registry secret if needed
|
||||||
// RefreshEcrSecret method checks if the namespace has any ECR registry
|
// RefreshEcrSecret method checks if the namespace has any ECR registry
|
||||||
// otherwise return nil
|
// otherwise return nil
|
||||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, payload.Namespace)
|
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, payload.Namespace)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
|
@ -95,7 +95,7 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
|
||||||
return httperror.InternalServerError("Unable to verify user authorizations to validate stack deletion", err)
|
return httperror.InternalServerError("Unable to verify user authorizations to validate stack deletion", err)
|
||||||
}
|
}
|
||||||
if !canManage {
|
if !canManage {
|
||||||
errMsg := "stack management is disabled for non-admin users"
|
errMsg := "Stack management is disabled for non-admin users"
|
||||||
return httperror.Forbidden(errMsg, errors.New(errMsg))
|
return httperror.Forbidden(errMsg, errors.New(errMsg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||||
// Refresh ECR registry secret if needed
|
// Refresh ECR registry secret if needed
|
||||||
// RefreshEcrSecret method checks if the namespace has any ECR registry
|
// RefreshEcrSecret method checks if the namespace has any ECR registry
|
||||||
// otherwise return nil
|
// otherwise return nil
|
||||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, stack.Namespace)
|
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, stack.Namespace)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ func (handler *Handler) updateUserServiceAccounts(membership *portainer.TeamMemb
|
||||||
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
|
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
|
||||||
// update kubernenets service accounts if the team is associated with a kubernetes environment
|
// update kubernenets service accounts if the team is associated with a kubernetes environment
|
||||||
if endpointutils.IsKubernetesEndpoint(&endpoint) {
|
if endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||||
kubecli, err := handler.K8sClientFactory.GetKubeClient(&endpoint)
|
kubecli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(&endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("failed getting kube client for environment %d", endpoint.ID)
|
log.Error().Err(err).Msgf("failed getting kube client for environment %d", endpoint.ID)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -102,7 +102,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to create Kubernetes client", err)
|
return httperror.InternalServerError("Unable to create Kubernetes client", err)
|
||||||
}
|
}
|
||||||
|
@ -165,7 +165,7 @@ func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endp
|
||||||
return "", false, err
|
return "", false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
kubecli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
kubecli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,12 +42,12 @@ func (handler *Handler) websocketShellPodExec(w http.ResponseWriter, r *http.Req
|
||||||
return httperror.Forbidden("Permission denied to access environment", err)
|
return httperror.Forbidden("Permission denied to access environment", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to create Kubernetes client", err)
|
return httperror.InternalServerError("Unable to create Kubernetes client", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceAccount, err := cli.GetServiceAccount(tokenData)
|
serviceAccount, err := cli.GetPortainerUserServiceAccount(tokenData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to find serviceaccount associated with user", err)
|
return httperror.InternalServerError("Unable to find serviceaccount associated with user", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,77 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
type (
|
import (
|
||||||
K8sApplication struct {
|
"time"
|
||||||
UID string `json:",omitempty"`
|
|
||||||
Name string `json:""`
|
corev1 "k8s.io/api/core/v1"
|
||||||
Namespace string `json:",omitempty"`
|
|
||||||
Kind string `json:",omitempty"`
|
|
||||||
Labels map[string]string `json:",omitempty"`
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type K8sApplication struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Image string `json:"Image"`
|
||||||
|
Containers []interface{} `json:"Containers,omitempty"`
|
||||||
|
Services []corev1.Service `json:"Services"`
|
||||||
|
CreationDate time.Time `json:"CreationDate"`
|
||||||
|
ApplicationOwner string `json:"ApplicationOwner,omitempty"`
|
||||||
|
StackName string `json:"StackName,omitempty"`
|
||||||
|
ResourcePool string `json:"ResourcePool"`
|
||||||
|
ApplicationType string `json:"ApplicationType"`
|
||||||
|
Metadata *Metadata `json:"Metadata,omitempty"`
|
||||||
|
Status string `json:"Status"`
|
||||||
|
TotalPodsCount int `json:"TotalPodsCount"`
|
||||||
|
RunningPodsCount int `json:"RunningPodsCount"`
|
||||||
|
DeploymentType string `json:"DeploymentType"`
|
||||||
|
Pods []Pod `json:"Pods,omitempty"`
|
||||||
|
Configurations []Configuration `json:"Configurations,omitempty"`
|
||||||
|
LoadBalancerIPAddress string `json:"LoadBalancerIPAddress,omitempty"`
|
||||||
|
PublishedPorts []PublishedPort `json:"PublishedPorts,omitempty"`
|
||||||
|
Namespace string `json:"Namespace,omitempty"`
|
||||||
|
UID string `json:"Uid,omitempty"`
|
||||||
|
StackID string `json:"StackId,omitempty"`
|
||||||
|
ServiceID string `json:"ServiceId,omitempty"`
|
||||||
|
ServiceName string `json:"ServiceName,omitempty"`
|
||||||
|
ServiceType string `json:"ServiceType,omitempty"`
|
||||||
|
Kind string `json:"Kind,omitempty"`
|
||||||
|
MatchLabels map[string]string `json:"MatchLabels,omitempty"`
|
||||||
|
Labels map[string]string `json:"Labels,omitempty"`
|
||||||
|
Resource K8sApplicationResource `json:"Resource,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pod struct {
|
||||||
|
Status string `json:"Status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Configuration struct {
|
||||||
|
Data map[string]interface{} `json:"Data,omitempty"`
|
||||||
|
Kind string `json:"Kind"`
|
||||||
|
ConfigurationOwner string `json:"ConfigurationOwner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishedPort struct {
|
||||||
|
IngressRules []IngressRule `json:"IngressRules"`
|
||||||
|
Port int `json:"Port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IngressRule struct {
|
||||||
|
Host string `json:"Host"`
|
||||||
|
IP string `json:"IP"`
|
||||||
|
Path string `json:"Path"`
|
||||||
|
TLS []TLSInfo `json:"TLS"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TLSInfo struct {
|
||||||
|
Hosts []string `json:"hosts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing types
|
||||||
|
type K8sApplicationResource struct {
|
||||||
|
CPURequest int64 `json:"CpuRequest"`
|
||||||
|
CPULimit int64 `json:"CpuLimit"`
|
||||||
|
MemoryRequest int64 `json:"MemoryRequest"`
|
||||||
|
MemoryLimit int64 `json:"MemoryLimit"`
|
||||||
|
}
|
||||||
|
|
16
api/http/models/kubernetes/cluster_role_bindings.go
Normal file
16
api/http/models/kubernetes/cluster_role_bindings.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
K8sClusterRoleBinding struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
RoleRef rbacv1.RoleRef `json:"roleRef"`
|
||||||
|
Subjects []rbacv1.Subject `json:"subjects"`
|
||||||
|
CreationDate time.Time `json:"creationDate"`
|
||||||
|
}
|
||||||
|
)
|
8
api/http/models/kubernetes/cluster_roles.go
Normal file
8
api/http/models/kubernetes/cluster_roles.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type K8sClusterRole struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreationDate time.Time `json:"creationDate"`
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
package kubernetes
|
|
||||||
|
|
||||||
type (
|
|
||||||
K8sConfigMapOrSecret struct {
|
|
||||||
UID string `json:"UID"`
|
|
||||||
Name string `json:"Name"`
|
|
||||||
Namespace string `json:"Namespace"`
|
|
||||||
CreationDate string `json:"CreationDate"`
|
|
||||||
Annotations map[string]string `json:"Annotations"`
|
|
||||||
Data map[string]string `json:"Data"`
|
|
||||||
Applications []string `json:"Applications"`
|
|
||||||
IsSecret bool `json:"IsSecret"`
|
|
||||||
|
|
||||||
// SecretType will be an empty string for config maps.
|
|
||||||
SecretType string `json:"SecretType"`
|
|
||||||
}
|
|
||||||
)
|
|
32
api/http/models/kubernetes/configuration.go
Normal file
32
api/http/models/kubernetes/configuration.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
type (
|
||||||
|
K8sConfigMap struct {
|
||||||
|
K8sConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sSecret struct {
|
||||||
|
K8sConfiguration
|
||||||
|
SecretType string `json:"SecretType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sConfiguration struct {
|
||||||
|
UID string `json:"UID"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Namespace string `json:"Namespace"`
|
||||||
|
CreationDate string `json:"CreationDate"`
|
||||||
|
Annotations map[string]string `json:"Annotations"`
|
||||||
|
Data map[string]string `json:"Data"`
|
||||||
|
IsUsed bool `json:"IsUsed"`
|
||||||
|
Labels map[string]string `json:"Labels"`
|
||||||
|
ConfigurationOwnerResources []K8sConfigurationOwnerResource `json:"ConfigurationOwners"`
|
||||||
|
ConfigurationOwner string `json:"ConfigurationOwner"`
|
||||||
|
ConfigurationOwnerId string `json:"ConfigurationOwnerId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sConfigurationOwnerResource struct {
|
||||||
|
Id string `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
ResourceKind string `json:"ResourceKind"`
|
||||||
|
}
|
||||||
|
)
|
|
@ -44,6 +44,7 @@ type (
|
||||||
Port int `json:"Port"`
|
Port int `json:"Port"`
|
||||||
Path string `json:"Path"`
|
Path string `json:"Path"`
|
||||||
PathType string `json:"PathType"`
|
PathType string `json:"PathType"`
|
||||||
|
HasService bool `json:"HasService"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// K8sIngressDeleteRequests is a mapping of namespace names to a slice of
|
// K8sIngressDeleteRequests is a mapping of namespace names to a slice of
|
||||||
|
|
17
api/http/models/kubernetes/role_bindings.go
Normal file
17
api/http/models/kubernetes/role_bindings.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
K8sRoleBinding struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
RoleRef rbacv1.RoleRef `json:"roleRef"`
|
||||||
|
Subjects []rbacv1.Subject `json:"subjects"`
|
||||||
|
CreationDate time.Time `json:"creationDate"`
|
||||||
|
}
|
||||||
|
)
|
9
api/http/models/kubernetes/roles.go
Normal file
9
api/http/models/kubernetes/roles.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type K8sRole struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
CreationDate time.Time `json:"creationDate"`
|
||||||
|
}
|
9
api/http/models/kubernetes/service_accounts.go
Normal file
9
api/http/models/kubernetes/service_accounts.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type K8sServiceAccount struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
CreationDate time.Time `json:"creationDate"`
|
||||||
|
}
|
|
@ -7,16 +7,16 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
K8sServiceInfo struct {
|
K8sServiceInfo struct {
|
||||||
Name string
|
Name string `json:",omitempty"`
|
||||||
UID string
|
UID string `json:",omitempty"`
|
||||||
Type string
|
Type string `json:",omitempty"`
|
||||||
Namespace string
|
Namespace string `json:",omitempty"`
|
||||||
Annotations map[string]string
|
Annotations map[string]string `json:",omitempty"`
|
||||||
CreationTimestamp string
|
CreationDate string `json:",omitempty"`
|
||||||
Labels map[string]string
|
Labels map[string]string `json:",omitempty"`
|
||||||
AllocateLoadBalancerNodePorts *bool `json:",omitempty"`
|
AllocateLoadBalancerNodePorts *bool `json:",omitempty"`
|
||||||
Ports []K8sServicePort
|
Ports []K8sServicePort `json:",omitempty"`
|
||||||
Selector map[string]string
|
Selector map[string]string `json:",omitempty"`
|
||||||
IngressStatus []K8sServiceIngress `json:",omitempty"`
|
IngressStatus []K8sServiceIngress `json:",omitempty"`
|
||||||
|
|
||||||
// serviceList screen
|
// serviceList screen
|
||||||
|
|
49
api/http/models/kubernetes/volumes.go
Normal file
49
api/http/models/kubernetes/volumes.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
K8sVolumeInfo struct {
|
||||||
|
PersistentVolume K8sPersistentVolume `json:"persistentVolume"`
|
||||||
|
PersistentVolumeClaim K8sPersistentVolumeClaim `json:"persistentVolumeClaim"`
|
||||||
|
StorageClass K8sStorageClass `json:"storageClass"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sPersistentVolume struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
|
AccessModes []corev1.PersistentVolumeAccessMode `json:"accessModes,omitempty"`
|
||||||
|
Capacity corev1.ResourceList `json:"capacity"`
|
||||||
|
ClaimRef *corev1.ObjectReference `json:"claimRef"`
|
||||||
|
StorageClassName string `json:"storageClassName,omitempty"`
|
||||||
|
PersistentVolumeReclaimPolicy corev1.PersistentVolumeReclaimPolicy `json:"persistentVolumeReclaimPolicy"`
|
||||||
|
VolumeMode *corev1.PersistentVolumeMode `json:"volumeMode"`
|
||||||
|
CSI *corev1.CSIPersistentVolumeSource `json:"csi,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sPersistentVolumeClaim struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Storage int64 `json:"storage"`
|
||||||
|
CreationDate time.Time `json:"creationDate"`
|
||||||
|
AccessModes []corev1.PersistentVolumeAccessMode `json:"accessModes,omitempty"`
|
||||||
|
VolumeName string `json:"volumeName"`
|
||||||
|
ResourcesRequests *corev1.ResourceList `json:"resourcesRequests"`
|
||||||
|
StorageClass *string `json:"storageClass"`
|
||||||
|
VolumeMode *corev1.PersistentVolumeMode `json:"volumeMode"`
|
||||||
|
OwningApplications []K8sApplication `json:"owningApplications,omitempty"`
|
||||||
|
Phase corev1.PersistentVolumeClaimPhase `json:"phase"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sStorageClass struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Provisioner string `json:"provisioner"`
|
||||||
|
ReclaimPolicy *corev1.PersistentVolumeReclaimPolicy `json:"reclaimPolicy"`
|
||||||
|
AllowVolumeExpansion *bool `json:"allowVolumeExpansion"`
|
||||||
|
}
|
||||||
|
)
|
|
@ -27,7 +27,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint)
|
kubecli, err := factory.kubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint)
|
kubecli, err := factory.kubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
|
||||||
|
|
||||||
remoteURL.Scheme = "https"
|
remoteURL.Scheme = "https"
|
||||||
|
|
||||||
kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint)
|
kubecli, err := factory.kubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (transport *baseTransport) refreshRegistry(request *http.Request, namespace string) (err error) {
|
func (transport *baseTransport) refreshRegistry(request *http.Request, namespace string) (err error) {
|
||||||
cli, err := transport.k8sClientFactory.GetKubeClient(transport.endpoint)
|
cli, err := transport.k8sClientFactory.GetPrivilegedKubeClient(transport.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,7 +142,7 @@ func (transport *baseTransport) getRoundTripToken(request *http.Request, tokenMa
|
||||||
} else {
|
} else {
|
||||||
token, err = tokenManager.GetUserServiceAccountToken(int(tokenData.ID), transport.endpoint.ID)
|
token, err = tokenManager.GetUserServiceAccountToken(int(tokenData.ID), transport.endpoint.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("failed retrieving service account token")
|
Msg("failed retrieving service account token")
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ func (service *Service) CleanNAPWithOverridePolicies(
|
||||||
endpoint *portainer.Endpoint,
|
endpoint *portainer.Endpoint,
|
||||||
endpointGroup *portainer.EndpointGroup,
|
endpointGroup *portainer.EndpointGroup,
|
||||||
) error {
|
) error {
|
||||||
kubecli, err := service.K8sClientFactory.GetKubeClient(endpoint)
|
kubecli, err := service.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cli, err := factory.GetKubeClient(endpoint)
|
cli, err := factory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("unable to create kubernetes client for ingress class detection")
|
log.Debug().Err(err).Msg("unable to create kubernetes client for ingress class detection")
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService datas
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cli, err := factory.GetKubeClient(endpoint)
|
cli, err := factory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("unable to create kubernetes client for initial metrics detection")
|
log.Debug().Err(err).Msg("unable to create kubernetes client for initial metrics detection")
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.En
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cli, err := factory.GetKubeClient(endpoint)
|
cli, err := factory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("unable to create Kubernetes client for initial storage detection")
|
log.Debug().Err(err).Msg("unable to create Kubernetes client for initial storage detection")
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
@ -21,24 +22,27 @@ func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
|
||||||
return errors.WithMessage(err, "failed to fetch access policies")
|
return errors.WithMessage(err, "failed to fetch access policies")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if policies != nil {
|
||||||
delete(policies, ns)
|
delete(policies, ns)
|
||||||
|
|
||||||
return kcl.UpdateNamespaceAccessPolicies(policies)
|
return kcl.UpdateNamespaceAccessPolicies(policies)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNamespaceAccessPolicies gets the namespace access policies
|
// GetNamespaceAccessPolicies gets the namespace access policies
|
||||||
// from config maps in the portainer namespace
|
// from config maps in the portainer namespace
|
||||||
func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) {
|
func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) {
|
||||||
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(context.TODO(), portainerConfigMapName, metav1.GetOptions{})
|
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(context.TODO(), portainerConfigMapName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
if k8serrors.IsNotFound(err) {
|
if k8serrors.IsNotFound(err) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if err != nil {
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
|
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
|
||||||
|
policies := map[string]portainer.K8sNamespaceAccessPolicy{}
|
||||||
var policies map[string]portainer.K8sNamespaceAccessPolicy
|
|
||||||
err = json.Unmarshal([]byte(accessData), &policies)
|
err = json.Unmarshal([]byte(accessData), &policies)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -109,10 +113,6 @@ func (kcl *KubeClient) UpdateNamespaceAccessPolicies(accessPolicies map[string]p
|
||||||
}
|
}
|
||||||
|
|
||||||
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(context.TODO(), portainerConfigMapName, metav1.GetOptions{})
|
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(context.TODO(), portainerConfigMapName, metav1.GetOptions{})
|
||||||
if k8serrors.IsNotFound(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -122,3 +122,20 @@ func (kcl *KubeClient) UpdateNamespaceAccessPolicies(accessPolicies map[string]p
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNonAdminNamespaces retrieves namespaces for a non-admin user, excluding the default namespace if restricted.
|
||||||
|
func (kcl *KubeClient) GetNonAdminNamespaces(userID int) ([]string, error) {
|
||||||
|
accessPolicies, err := kcl.GetNamespaceAccessPolicies()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("an error occurred during the getNonAdminNamespaces operation, unable to get namespace access policies via portainer-config. check if portainer-config configMap exists in the Kubernetes cluster: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaces := []string{defaultNamespace}
|
||||||
|
for namespace, accessPolicy := range accessPolicies {
|
||||||
|
if hasUserAccessToNamespace(userID, nil, accessPolicy) {
|
||||||
|
nonAdminNamespaces = append(nonAdminNamespaces, namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonAdminNamespaces, nil
|
||||||
|
}
|
||||||
|
|
|
@ -2,153 +2,440 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"fmt"
|
||||||
|
|
||||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
labels "k8s.io/apimachinery/pkg/labels"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetApplications gets a list of kubernetes workloads (or applications) by kind. If Kind is not specified, gets the all
|
// GetAllKubernetesApplications gets a list of kubernetes workloads (or applications) across all namespaces in the cluster
|
||||||
func (kcl *KubeClient) GetApplications(namespace, kind string) ([]models.K8sApplication, error) {
|
// if the user is an admin, all namespaces in the current k8s environment(endpoint) are fetched using the fetchApplications function.
|
||||||
applicationList := []models.K8sApplication{}
|
// otherwise, namespaces the non-admin user has access to will be used to filter the applications based on the allowed namespaces.
|
||||||
listOpts := metav1.ListOptions{}
|
func (kcl *KubeClient) GetApplications(namespace, nodeName string, withDependencies bool) ([]models.K8sApplication, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchApplications(namespace, nodeName, withDependencies)
|
||||||
|
}
|
||||||
|
|
||||||
if kind == "" || strings.EqualFold(kind, "deployment") {
|
return kcl.fetchApplicationsForNonAdmin(namespace, nodeName, withDependencies)
|
||||||
deployments, err := kcl.cli.AppsV1().Deployments(namespace).List(context.TODO(), listOpts)
|
}
|
||||||
|
|
||||||
|
// fetchApplications fetches the applications in the namespaces the user has access to.
|
||||||
|
// This function is called when the user is an admin.
|
||||||
|
func (kcl *KubeClient) fetchApplications(namespace, nodeName string, withDependencies bool) ([]models.K8sApplication, error) {
|
||||||
|
podListOptions := metav1.ListOptions{}
|
||||||
|
if nodeName != "" {
|
||||||
|
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", nodeName)
|
||||||
|
}
|
||||||
|
if !withDependencies {
|
||||||
|
// TODO: make sure not to fetch services in fetchAllApplicationsListResources from this call
|
||||||
|
pods, replicaSets, deployments, statefulSets, daemonSets, _, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range deployments.Items {
|
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, nil)
|
||||||
applicationList = append(applicationList, models.K8sApplication{
|
|
||||||
UID: string(d.UID),
|
|
||||||
Name: d.Name,
|
|
||||||
Namespace: d.Namespace,
|
|
||||||
Kind: "Deployment",
|
|
||||||
Labels: d.Labels,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind == "" || strings.EqualFold(kind, "statefulset") {
|
pods, replicaSets, deployments, statefulSets, daemonSets, services, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||||
statefulSets, err := kcl.cli.AppsV1().StatefulSets(namespace).List(context.TODO(), listOpts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range statefulSets.Items {
|
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services)
|
||||||
applicationList = append(applicationList, models.K8sApplication{
|
}
|
||||||
UID: string(s.UID),
|
|
||||||
Name: s.Name,
|
// fetchApplicationsForNonAdmin fetches the applications in the namespaces the user has access to.
|
||||||
Namespace: s.Namespace,
|
// This function is called when the user is not an admin.
|
||||||
Kind: "StatefulSet",
|
func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string, withDependencies bool) ([]models.K8sApplication, error) {
|
||||||
Labels: s.Labels,
|
log.Debug().Msgf("Fetching applications for non-admin user: %v", kcl.NonAdminNamespaces)
|
||||||
})
|
|
||||||
}
|
if len(kcl.NonAdminNamespaces) == 0 {
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind == "" || strings.EqualFold(kind, "daemonset") {
|
podListOptions := metav1.ListOptions{}
|
||||||
daemonSets, err := kcl.cli.AppsV1().DaemonSets(namespace).List(context.TODO(), listOpts)
|
if nodeName != "" {
|
||||||
|
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", nodeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !withDependencies {
|
||||||
|
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets(namespace, podListOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range daemonSets.Items {
|
return kcl.convertPodsToApplications(pods, replicaSets, nil, nil, nil, nil)
|
||||||
applicationList = append(applicationList, models.K8sApplication{
|
}
|
||||||
UID: string(d.UID),
|
|
||||||
Name: d.Name,
|
pods, replicaSets, deployments, statefulSets, daemonSets, services, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||||
Namespace: d.Namespace,
|
if err != nil {
|
||||||
Kind: "DaemonSet",
|
return nil, err
|
||||||
Labels: d.Labels,
|
}
|
||||||
})
|
|
||||||
|
applications, err := kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := make([]models.K8sApplication, 0)
|
||||||
|
for _, application := range applications {
|
||||||
|
if _, ok := nonAdminNamespaceSet[application.ResourcePool]; ok {
|
||||||
|
results = append(results, application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind == "" || strings.EqualFold(kind, "nakedpods") {
|
return results, nil
|
||||||
pods, _ := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})
|
}
|
||||||
|
|
||||||
|
// convertPodsToApplications processes pods and converts them to applications, ensuring uniqueness by owner reference.
|
||||||
|
func (kcl *KubeClient) convertPodsToApplications(pods []corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service) ([]models.K8sApplication, error) {
|
||||||
|
applications := []models.K8sApplication{}
|
||||||
|
processedOwners := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, pod := range pods {
|
||||||
|
if len(pod.OwnerReferences) > 0 {
|
||||||
|
ownerUID := string(pod.OwnerReferences[0].UID)
|
||||||
|
if _, exists := processedOwners[ownerUID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processedOwners[ownerUID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
application, err := kcl.ConvertPodToApplication(pod, replicaSets, deployments, statefulSets, daemonSets, services, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if application != nil {
|
||||||
|
applications = append(applications, *application)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return applications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClusterApplicationsResource returns the total resource requests and limits for all applications in a namespace
|
||||||
|
// for a cluster level resource, set the namespace to ""
|
||||||
|
func (kcl *KubeClient) GetApplicationsResource(namespace, node string) (models.K8sApplicationResource, error) {
|
||||||
|
resource := models.K8sApplicationResource{}
|
||||||
|
podListOptions := metav1.ListOptions{}
|
||||||
|
if node != "" {
|
||||||
|
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), podListOptions)
|
||||||
|
if err != nil {
|
||||||
|
return resource, err
|
||||||
|
}
|
||||||
|
|
||||||
for _, pod := range pods.Items {
|
for _, pod := range pods.Items {
|
||||||
naked := false
|
for _, container := range pod.Spec.Containers {
|
||||||
if len(pod.OwnerReferences) == 0 {
|
resource.CPURequest += container.Resources.Requests.Cpu().MilliValue()
|
||||||
naked = true
|
resource.CPULimit += container.Resources.Limits.Cpu().MilliValue()
|
||||||
} else {
|
resource.MemoryRequest += container.Resources.Requests.Memory().Value()
|
||||||
managed := false
|
resource.MemoryLimit += container.Resources.Limits.Memory().Value()
|
||||||
loop:
|
|
||||||
for _, ownerRef := range pod.OwnerReferences {
|
|
||||||
switch ownerRef.Kind {
|
|
||||||
case "Deployment", "DaemonSet", "ReplicaSet":
|
|
||||||
managed = true
|
|
||||||
break loop
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !managed {
|
return resource, nil
|
||||||
naked = true
|
}
|
||||||
|
|
||||||
|
// convertApplicationResourceUnits converts the resource units from milli to core and bytes to mega bytes
|
||||||
|
func convertApplicationResourceUnits(resource models.K8sApplicationResource) models.K8sApplicationResource {
|
||||||
|
return models.K8sApplicationResource{
|
||||||
|
CPURequest: resource.CPURequest / 1000,
|
||||||
|
CPULimit: resource.CPULimit / 1000,
|
||||||
|
MemoryRequest: resource.MemoryRequest / 1024 / 1024,
|
||||||
|
MemoryLimit: resource.MemoryLimit / 1024 / 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApplicationsFromConfigMap gets a list of applications that use a specific ConfigMap
|
||||||
|
// by checking all pods in the same namespace as the ConfigMap
|
||||||
|
func (kcl *KubeClient) GetApplicationNamesFromConfigMap(configMap models.K8sConfigMap, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]string, error) {
|
||||||
|
applications := []string{}
|
||||||
|
for _, pod := range pods {
|
||||||
|
if pod.Namespace == configMap.Namespace {
|
||||||
|
if isPodUsingConfigMap(&pod, configMap.Name) {
|
||||||
|
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
applications = append(applications, application.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if naked {
|
return applications, nil
|
||||||
applicationList = append(applicationList, models.K8sApplication{
|
}
|
||||||
UID: string(pod.UID),
|
|
||||||
Name: pod.Name,
|
func (kcl *KubeClient) GetApplicationNamesFromSecret(secret models.K8sSecret, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]string, error) {
|
||||||
Namespace: pod.Namespace,
|
applications := []string{}
|
||||||
Kind: "Pod",
|
for _, pod := range pods {
|
||||||
|
if pod.Namespace == secret.Namespace {
|
||||||
|
if isPodUsingSecret(&pod, secret.Name) {
|
||||||
|
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
applications = append(applications, application.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return applications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertPodToApplication converts a pod to an application, updating owner references if necessary
|
||||||
|
func (kcl *KubeClient) ConvertPodToApplication(pod corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service, withResource bool) (*models.K8sApplication, error) {
|
||||||
|
if isReplicaSetOwner(pod) {
|
||||||
|
updateOwnerReferenceToDeployment(&pod, replicaSets)
|
||||||
|
}
|
||||||
|
|
||||||
|
application := createApplication(&pod, deployments, statefulSets, daemonSets, services)
|
||||||
|
if application.ID == "" && application.Name == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if withResource {
|
||||||
|
application.Resource = calculateResourceUsage(pod)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &application, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createApplication creates a K8sApplication object from a pod
|
||||||
|
// it sets the application name, namespace, kind, image, stack id, stack name, and labels
|
||||||
|
func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service) models.K8sApplication {
|
||||||
|
kind := "Pod"
|
||||||
|
name := pod.Name
|
||||||
|
|
||||||
|
if len(pod.OwnerReferences) > 0 {
|
||||||
|
kind = pod.OwnerReferences[0].Kind
|
||||||
|
name = pod.OwnerReferences[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
application := models.K8sApplication{
|
||||||
|
Services: []corev1.Service{},
|
||||||
|
Metadata: &models.Metadata{},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case "Deployment":
|
||||||
|
for _, deployment := range deployments {
|
||||||
|
if deployment.Name == name && deployment.Namespace == pod.Namespace {
|
||||||
|
application.ApplicationType = "Deployment"
|
||||||
|
application.Kind = "Deployment"
|
||||||
|
application.ID = string(deployment.UID)
|
||||||
|
application.ResourcePool = deployment.Namespace
|
||||||
|
application.Name = name
|
||||||
|
application.Image = deployment.Spec.Template.Spec.Containers[0].Image
|
||||||
|
application.ApplicationOwner = deployment.Labels["io.portainer.kubernetes.application.owner"]
|
||||||
|
application.StackID = deployment.Labels["io.portainer.kubernetes.application.stackid"]
|
||||||
|
application.StackName = deployment.Labels["io.portainer.kubernetes.application.stack"]
|
||||||
|
application.Labels = deployment.Labels
|
||||||
|
application.MatchLabels = deployment.Spec.Selector.MatchLabels
|
||||||
|
application.CreationDate = deployment.CreationTimestamp.Time
|
||||||
|
application.TotalPodsCount = int(deployment.Status.Replicas)
|
||||||
|
application.RunningPodsCount = int(deployment.Status.ReadyReplicas)
|
||||||
|
application.DeploymentType = "Replicated"
|
||||||
|
application.Metadata = &models.Metadata{
|
||||||
|
Labels: deployment.Labels,
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "StatefulSet":
|
||||||
|
for _, statefulSet := range statefulSets {
|
||||||
|
if statefulSet.Name == name && statefulSet.Namespace == pod.Namespace {
|
||||||
|
application.Kind = "StatefulSet"
|
||||||
|
application.ApplicationType = "StatefulSet"
|
||||||
|
application.ID = string(statefulSet.UID)
|
||||||
|
application.ResourcePool = statefulSet.Namespace
|
||||||
|
application.Name = name
|
||||||
|
application.Image = statefulSet.Spec.Template.Spec.Containers[0].Image
|
||||||
|
application.ApplicationOwner = statefulSet.Labels["io.portainer.kubernetes.application.owner"]
|
||||||
|
application.StackID = statefulSet.Labels["io.portainer.kubernetes.application.stackid"]
|
||||||
|
application.StackName = statefulSet.Labels["io.portainer.kubernetes.application.stack"]
|
||||||
|
application.Labels = statefulSet.Labels
|
||||||
|
application.MatchLabels = statefulSet.Spec.Selector.MatchLabels
|
||||||
|
application.CreationDate = statefulSet.CreationTimestamp.Time
|
||||||
|
application.TotalPodsCount = int(statefulSet.Status.Replicas)
|
||||||
|
application.RunningPodsCount = int(statefulSet.Status.ReadyReplicas)
|
||||||
|
application.DeploymentType = "Replicated"
|
||||||
|
application.Metadata = &models.Metadata{
|
||||||
|
Labels: statefulSet.Labels,
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DaemonSet":
|
||||||
|
for _, daemonSet := range daemonSets {
|
||||||
|
if daemonSet.Name == name && daemonSet.Namespace == pod.Namespace {
|
||||||
|
application.Kind = "DaemonSet"
|
||||||
|
application.ApplicationType = "DaemonSet"
|
||||||
|
application.ID = string(daemonSet.UID)
|
||||||
|
application.ResourcePool = daemonSet.Namespace
|
||||||
|
application.Name = name
|
||||||
|
application.Image = daemonSet.Spec.Template.Spec.Containers[0].Image
|
||||||
|
application.ApplicationOwner = daemonSet.Labels["io.portainer.kubernetes.application.owner"]
|
||||||
|
application.StackID = daemonSet.Labels["io.portainer.kubernetes.application.stackid"]
|
||||||
|
application.StackName = daemonSet.Labels["io.portainer.kubernetes.application.stack"]
|
||||||
|
application.Labels = daemonSet.Labels
|
||||||
|
application.MatchLabels = daemonSet.Spec.Selector.MatchLabels
|
||||||
|
application.CreationDate = daemonSet.CreationTimestamp.Time
|
||||||
|
application.TotalPodsCount = int(daemonSet.Status.DesiredNumberScheduled)
|
||||||
|
application.RunningPodsCount = int(daemonSet.Status.NumberReady)
|
||||||
|
application.DeploymentType = "Global"
|
||||||
|
application.Metadata = &models.Metadata{
|
||||||
|
Labels: daemonSet.Labels,
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Pod":
|
||||||
|
runningPodsCount := 1
|
||||||
|
if pod.Status.Phase != corev1.PodRunning {
|
||||||
|
runningPodsCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
application.ApplicationType = "Pod"
|
||||||
|
application.Kind = "Pod"
|
||||||
|
application.ID = string(pod.UID)
|
||||||
|
application.ResourcePool = pod.Namespace
|
||||||
|
application.Name = pod.Name
|
||||||
|
application.Image = pod.Spec.Containers[0].Image
|
||||||
|
application.ApplicationOwner = pod.Labels["io.portainer.kubernetes.application.owner"]
|
||||||
|
application.StackID = pod.Labels["io.portainer.kubernetes.application.stackid"]
|
||||||
|
application.StackName = pod.Labels["io.portainer.kubernetes.application.stack"]
|
||||||
|
application.Labels = pod.Labels
|
||||||
|
application.MatchLabels = pod.Labels
|
||||||
|
application.CreationDate = pod.CreationTimestamp.Time
|
||||||
|
application.TotalPodsCount = 1
|
||||||
|
application.RunningPodsCount = runningPodsCount
|
||||||
|
application.DeploymentType = string(pod.Status.Phase)
|
||||||
|
application.Metadata = &models.Metadata{
|
||||||
Labels: pod.Labels,
|
Labels: pod.Labels,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if application.ID != "" && application.Name != "" && len(services) > 0 {
|
||||||
|
return updateApplicationWithService(application, services)
|
||||||
|
}
|
||||||
|
|
||||||
|
return application
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateApplicationWithService updates the application with the services that match the application's selector match labels
|
||||||
|
// and are in the same namespace as the application
|
||||||
|
func updateApplicationWithService(application models.K8sApplication, services []corev1.Service) models.K8sApplication {
|
||||||
|
for _, service := range services {
|
||||||
|
serviceSelector := labels.SelectorFromSet(service.Spec.Selector)
|
||||||
|
|
||||||
|
if service.Namespace == application.ResourcePool && serviceSelector.Matches(labels.Set(application.MatchLabels)) {
|
||||||
|
application.ServiceType = string(service.Spec.Type)
|
||||||
|
application.Services = append(application.Services, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return application
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateResourceUsage calculates the resource usage for a pod
|
||||||
|
func calculateResourceUsage(pod corev1.Pod) models.K8sApplicationResource {
|
||||||
|
resource := models.K8sApplicationResource{}
|
||||||
|
for _, container := range pod.Spec.Containers {
|
||||||
|
resource.CPURequest += container.Resources.Requests.Cpu().MilliValue()
|
||||||
|
resource.CPULimit += container.Resources.Limits.Cpu().MilliValue()
|
||||||
|
resource.MemoryRequest += container.Resources.Requests.Memory().Value()
|
||||||
|
resource.MemoryLimit += container.Resources.Limits.Memory().Value()
|
||||||
|
}
|
||||||
|
return convertApplicationResourceUnits(resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApplicationFromServiceSelector gets applications based on service selectors
|
||||||
|
// it matches the service selector with the pod labels
|
||||||
|
func (kcl *KubeClient) GetApplicationFromServiceSelector(pods []corev1.Pod, service models.K8sServiceInfo, replicaSets []appsv1.ReplicaSet) (*models.K8sApplication, error) {
|
||||||
|
servicesSelector := labels.SelectorFromSet(service.Selector)
|
||||||
|
if servicesSelector.Empty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pod := range pods {
|
||||||
|
if servicesSelector.Matches(labels.Set(pod.Labels)) {
|
||||||
|
if isReplicaSetOwner(pod) {
|
||||||
|
updateOwnerReferenceToDeployment(&pod, replicaSets)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.K8sApplication{
|
||||||
|
Name: pod.OwnerReferences[0].Name,
|
||||||
|
Kind: pod.OwnerReferences[0].Kind,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApplicationConfigurationOwnersFromConfigMap gets a list of applications that use a specific ConfigMap
|
||||||
|
// by checking all pods in the same namespace as the ConfigMap
|
||||||
|
func (kcl *KubeClient) GetApplicationConfigurationOwnersFromConfigMap(configMap models.K8sConfigMap, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]models.K8sConfigurationOwnerResource, error) {
|
||||||
|
configurationOwners := []models.K8sConfigurationOwnerResource{}
|
||||||
|
for _, pod := range pods {
|
||||||
|
if pod.Namespace == configMap.Namespace {
|
||||||
|
if isPodUsingConfigMap(&pod, configMap.Name) {
|
||||||
|
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if application != nil {
|
||||||
|
configurationOwners = append(configurationOwners, models.K8sConfigurationOwnerResource{
|
||||||
|
Name: application.Name,
|
||||||
|
ResourceKind: application.Kind,
|
||||||
|
Id: application.UID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return applicationList, nil
|
return configurationOwners, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetApplication gets a kubernetes workload (application) by kind and name. If Kind is not specified, gets the all
|
// GetApplicationConfigurationOwnersFromSecret gets a list of applications that use a specific Secret
|
||||||
func (kcl *KubeClient) GetApplication(namespace, kind, name string) (models.K8sApplication, error) {
|
// by checking all pods in the same namespace as the Secret
|
||||||
|
func (kcl *KubeClient) GetApplicationConfigurationOwnersFromSecret(secret models.K8sSecret, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]models.K8sConfigurationOwnerResource, error) {
|
||||||
opts := metav1.GetOptions{}
|
configurationOwners := []models.K8sConfigurationOwnerResource{}
|
||||||
|
for _, pod := range pods {
|
||||||
switch strings.ToLower(kind) {
|
if pod.Namespace == secret.Namespace {
|
||||||
case "deployment":
|
if isPodUsingSecret(&pod, secret.Name) {
|
||||||
d, err := kcl.cli.AppsV1().Deployments(namespace).Get(context.TODO(), name, opts)
|
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.K8sApplication{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return models.K8sApplication{
|
if application != nil {
|
||||||
UID: string(d.UID),
|
configurationOwners = append(configurationOwners, models.K8sConfigurationOwnerResource{
|
||||||
Name: d.Name,
|
Name: application.Name,
|
||||||
Namespace: d.Namespace,
|
ResourceKind: application.Kind,
|
||||||
Kind: "Deployment",
|
Id: application.UID,
|
||||||
Labels: d.Labels,
|
})
|
||||||
}, nil
|
}
|
||||||
|
}
|
||||||
case "statefulset":
|
}
|
||||||
s, err := kcl.cli.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, opts)
|
|
||||||
if err != nil {
|
|
||||||
return models.K8sApplication{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return models.K8sApplication{
|
return configurationOwners, nil
|
||||||
UID: string(s.UID),
|
|
||||||
Name: s.Name,
|
|
||||||
Namespace: s.Namespace,
|
|
||||||
Kind: "StatefulSet",
|
|
||||||
Labels: s.Labels,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
case "daemonset":
|
|
||||||
d, err := kcl.cli.AppsV1().DaemonSets(namespace).Get(context.TODO(), name, opts)
|
|
||||||
if err != nil {
|
|
||||||
return models.K8sApplication{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return models.K8sApplication{
|
|
||||||
UID: string(d.UID),
|
|
||||||
Name: d.Name,
|
|
||||||
Namespace: d.Namespace,
|
|
||||||
Kind: "DaemonSet",
|
|
||||||
Labels: d.Labels,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return models.K8sApplication{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultKubeClientQPS = 30
|
defaultKubeClientQPS = 30
|
||||||
DefaultKubeClientBurst = 100
|
defaultKubeClientBurst = 100
|
||||||
|
maxConcurrency = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxConcurrency = 30
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// ClientFactory is used to create Kubernetes clients
|
// ClientFactory is used to create Kubernetes clients
|
||||||
ClientFactory struct {
|
ClientFactory struct {
|
||||||
|
@ -34,10 +33,8 @@ type (
|
||||||
reverseTunnelService portainer.ReverseTunnelService
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
instanceID string
|
instanceID string
|
||||||
endpointClients map[string]*KubeClient
|
|
||||||
endpointProxyClients *cache.Cache
|
endpointProxyClients *cache.Cache
|
||||||
AddrHTTPS string
|
AddrHTTPS string
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubeClient represent a service used to execute Kubernetes operations
|
// KubeClient represent a service used to execute Kubernetes operations
|
||||||
|
@ -45,6 +42,8 @@ type (
|
||||||
cli kubernetes.Interface
|
cli kubernetes.Interface
|
||||||
instanceID string
|
instanceID string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
IsKubeAdmin bool
|
||||||
|
NonAdminNamespaces []string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,7 +69,6 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
reverseTunnelService: reverseTunnelService,
|
reverseTunnelService: reverseTunnelService,
|
||||||
instanceID: instanceID,
|
instanceID: instanceID,
|
||||||
endpointClients: make(map[string]*KubeClient),
|
|
||||||
endpointProxyClients: cache.New(timeout, timeout),
|
endpointProxyClients: cache.New(timeout, timeout),
|
||||||
AddrHTTPS: addrHTTPS,
|
AddrHTTPS: addrHTTPS,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -80,42 +78,33 @@ func (factory *ClientFactory) GetInstanceID() (instanceID string) {
|
||||||
return factory.instanceID
|
return factory.instanceID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the cached kube client so a new one can be created
|
// Clear removes all cached kube clients
|
||||||
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
|
func (factory *ClientFactory) ClearClientCache() {
|
||||||
factory.mu.Lock()
|
log.Debug().Msgf("kubernetes namespace permissions have changed, clearing the client cache")
|
||||||
delete(factory.endpointClients, strconv.Itoa(int(endpointID)))
|
factory.endpointProxyClients.Flush()
|
||||||
factory.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
|
// Remove the cached kube client so a new one can be created
|
||||||
// If no client is registered, it will create a new client, register it, and returns it.
|
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
|
||||||
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
factory.endpointProxyClients.Delete(strconv.Itoa(int(endpointID)))
|
||||||
factory.mu.Lock()
|
}
|
||||||
key := strconv.Itoa(int(endpoint.ID))
|
|
||||||
if client, ok := factory.endpointClients[key]; ok {
|
|
||||||
factory.mu.Unlock()
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
factory.mu.Unlock()
|
|
||||||
|
|
||||||
// EE-6901: Do not lock
|
// GetPrivilegedKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
|
||||||
client, err := factory.createCachedAdminKubeClient(endpoint)
|
// If no client is registered, it will create a new client, register it, and returns it.
|
||||||
|
func (factory *ClientFactory) GetPrivilegedKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
||||||
|
key := strconv.Itoa(int(endpoint.ID))
|
||||||
|
pcl, ok := factory.endpointProxyClients.Get(key)
|
||||||
|
if ok {
|
||||||
|
return pcl.(*KubeClient), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
kcl, err := factory.createCachedPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
factory.mu.Lock()
|
factory.endpointProxyClients.Set(key, kcl, cache.DefaultExpiration)
|
||||||
defer factory.mu.Unlock()
|
return kcl, nil
|
||||||
|
|
||||||
// The lock was released before the client was created,
|
|
||||||
// so we need to check again
|
|
||||||
if c, ok := factory.endpointClients[key]; ok {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
factory.endpointClients[key] = client
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProxyKubeClient retrieves a KubeClient from the cache. You should be
|
// GetProxyKubeClient retrieves a KubeClient from the cache. You should be
|
||||||
|
@ -123,46 +112,47 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (*Kube
|
||||||
// kubernetes middleware.
|
// kubernetes middleware.
|
||||||
func (factory *ClientFactory) GetProxyKubeClient(endpointID, userID string) (*KubeClient, bool) {
|
func (factory *ClientFactory) GetProxyKubeClient(endpointID, userID string) (*KubeClient, bool) {
|
||||||
client, ok := factory.endpointProxyClients.Get(endpointID + "." + userID)
|
client, ok := factory.endpointProxyClients.Get(endpointID + "." + userID)
|
||||||
if !ok {
|
if ok {
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.(*KubeClient), true
|
return client.(*KubeClient), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetProxyKubeClient stores a kubeclient in the cache.
|
// SetProxyKubeClient stores a kubeclient in the cache.
|
||||||
func (factory *ClientFactory) SetProxyKubeClient(endpointID, userID string, cli *KubeClient) {
|
func (factory *ClientFactory) SetProxyKubeClient(endpointID, userID string, cli *KubeClient) {
|
||||||
factory.endpointProxyClients.Set(endpointID+"."+userID, cli, 0)
|
factory.endpointProxyClients.Set(endpointID+"."+userID, cli, cache.DefaultExpiration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateKubeClientFromKubeConfig creates a KubeClient from a clusterID, and
|
// CreateKubeClientFromKubeConfig creates a KubeClient from a clusterID, and
|
||||||
// Kubernetes config.
|
// Kubernetes config.
|
||||||
func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, kubeConfig []byte) (*KubeClient, error) {
|
func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, kubeConfig []byte, IsKubeAdmin bool, NonAdminNamespaces []string) (*KubeClient, error) {
|
||||||
config, err := clientcmd.NewClientConfigFromBytes(kubeConfig)
|
config, err := clientcmd.NewClientConfigFromBytes(kubeConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to create a client config from kubeconfig: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cliConfig, err := config.ClientConfig()
|
clientConfig, err := config.ClientConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to get the complete client config from kubeconfig: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cliConfig.QPS = DefaultKubeClientQPS
|
clientConfig.QPS = defaultKubeClientQPS
|
||||||
cliConfig.Burst = DefaultKubeClientBurst
|
clientConfig.Burst = defaultKubeClientBurst
|
||||||
|
|
||||||
cli, err := kubernetes.NewForConfig(cliConfig)
|
cli, err := kubernetes.NewForConfig(clientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to create a new clientset for the given config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &KubeClient{
|
return &KubeClient{
|
||||||
cli: cli,
|
cli: cli,
|
||||||
instanceID: factory.instanceID,
|
instanceID: factory.instanceID,
|
||||||
|
IsKubeAdmin: IsKubeAdmin,
|
||||||
|
NonAdminNamespaces: NonAdminNamespaces,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
func (factory *ClientFactory) createCachedPrivilegedKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
||||||
cli, err := factory.CreateClient(endpoint)
|
cli, err := factory.CreateClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -235,8 +225,8 @@ func (factory *ClientFactory) buildAgentConfig(endpoint *portainer.Endpoint) (*r
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Insecure = true
|
config.Insecure = true
|
||||||
config.QPS = DefaultKubeClientQPS
|
config.QPS = defaultKubeClientQPS
|
||||||
config.Burst = DefaultKubeClientBurst
|
config.Burst = defaultKubeClientBurst
|
||||||
|
|
||||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||||
return &agentHeaderRoundTripper{
|
return &agentHeaderRoundTripper{
|
||||||
|
@ -251,7 +241,7 @@ func (factory *ClientFactory) buildAgentConfig(endpoint *portainer.Endpoint) (*r
|
||||||
func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*rest.Config, error) {
|
func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*rest.Config, error) {
|
||||||
tunnelAddr, err := factory.reverseTunnelService.TunnelAddr(endpoint)
|
tunnelAddr, err := factory.reverseTunnelService.TunnelAddr(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed activating tunnel")
|
return nil, errors.Wrap(err, "failed to activate the chisel reverse tunnel. check if the tunnel port is open at the portainer instance")
|
||||||
}
|
}
|
||||||
endpointURL := fmt.Sprintf("http://%s/kubernetes", tunnelAddr)
|
endpointURL := fmt.Sprintf("http://%s/kubernetes", tunnelAddr)
|
||||||
|
|
||||||
|
@ -266,8 +256,8 @@ func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*re
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Insecure = true
|
config.Insecure = true
|
||||||
config.QPS = DefaultKubeClientQPS
|
config.QPS = defaultKubeClientQPS
|
||||||
config.Burst = DefaultKubeClientBurst
|
config.Burst = defaultKubeClientBurst
|
||||||
|
|
||||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||||
return &agentHeaderRoundTripper{
|
return &agentHeaderRoundTripper{
|
||||||
|
@ -294,8 +284,8 @@ func buildLocalConfig() (*rest.Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.QPS = DefaultKubeClientQPS
|
config.QPS = defaultKubeClientQPS
|
||||||
config.Burst = DefaultKubeClientBurst
|
config.Burst = defaultKubeClientBurst
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
43
api/kubernetes/cli/cluster_role.go
Normal file
43
api/kubernetes/cli/cluster_role.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetClusterRoles gets all the clusterRoles for at the cluster level in a k8s endpoint.
|
||||||
|
// It returns a list of K8sClusterRole objects.
|
||||||
|
func (kcl *KubeClient) GetClusterRoles() ([]models.K8sClusterRole, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchClusterRoles()
|
||||||
|
}
|
||||||
|
|
||||||
|
return []models.K8sClusterRole{}, fmt.Errorf("non-admin users are not allowed to access cluster roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchClusterRoles returns a list of all Roles in the specified namespace.
|
||||||
|
func (kcl *KubeClient) fetchClusterRoles() ([]models.K8sClusterRole, error) {
|
||||||
|
clusterRoles, err := kcl.cli.RbacV1().ClusterRoles().List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]models.K8sClusterRole, 0)
|
||||||
|
for _, clusterRole := range clusterRoles.Items {
|
||||||
|
results = append(results, parseClusterRole(clusterRole))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseClusterRole converts a rbacv1.ClusterRole object to a models.K8sClusterRole object.
|
||||||
|
func parseClusterRole(clusterRole rbacv1.ClusterRole) models.K8sClusterRole {
|
||||||
|
return models.K8sClusterRole{
|
||||||
|
Name: clusterRole.Name,
|
||||||
|
CreationDate: clusterRole.CreationTimestamp.Time,
|
||||||
|
}
|
||||||
|
}
|
45
api/kubernetes/cli/cluster_role_binding.go
Normal file
45
api/kubernetes/cli/cluster_role_binding.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetClusterRoleBindings gets all the clusterRoleBindings for at the cluster level in a k8s endpoint.
|
||||||
|
// It returns a list of K8sClusterRoleBinding objects.
|
||||||
|
func (kcl *KubeClient) GetClusterRoleBindings() ([]models.K8sClusterRoleBinding, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchClusterRoleBindings()
|
||||||
|
}
|
||||||
|
|
||||||
|
return []models.K8sClusterRoleBinding{}, fmt.Errorf("non-admin users are not allowed to access cluster role bindings")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchClusterRoleBindings returns a list of all cluster roles in the cluster.
|
||||||
|
func (kcl *KubeClient) fetchClusterRoleBindings() ([]models.K8sClusterRoleBinding, error) {
|
||||||
|
clusterRoleBindings, err := kcl.cli.RbacV1().ClusterRoleBindings().List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]models.K8sClusterRoleBinding, 0)
|
||||||
|
for _, clusterRoleBinding := range clusterRoleBindings.Items {
|
||||||
|
results = append(results, parseClusterRoleBinding(clusterRoleBinding))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseClusterRoleBinding converts a rbacv1.ClusterRoleBinding object to a models.K8sClusterRoleBinding object.
|
||||||
|
func parseClusterRoleBinding(clusterRoleBinding rbacv1.ClusterRoleBinding) models.K8sClusterRoleBinding {
|
||||||
|
return models.K8sClusterRoleBinding{
|
||||||
|
Name: clusterRoleBinding.Name,
|
||||||
|
RoleRef: clusterRoleBinding.RoleRef,
|
||||||
|
Subjects: clusterRoleBinding.Subjects,
|
||||||
|
CreationDate: clusterRoleBinding.CreationTimestamp.Time,
|
||||||
|
}
|
||||||
|
}
|
161
api/kubernetes/cli/configmap.go
Normal file
161
api/kubernetes/cli/configmap.go
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetConfigMaps gets all the ConfigMaps for a given namespace in a k8s endpoint.
|
||||||
|
// if the user is an admin, all configMaps in the current k8s environment(endpoint) are fetched using the fetchConfigMaps function.
|
||||||
|
// otherwise, namespaces the non-admin user has access to will be used to filter the configMaps based on the allowed namespaces.
|
||||||
|
func (kcl *KubeClient) GetConfigMaps(namespace string) ([]models.K8sConfigMap, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchConfigMaps(namespace)
|
||||||
|
}
|
||||||
|
return kcl.fetchConfigMapsForNonAdmin(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchConfigMapsForNonAdmin fetches the configMaps in the namespaces the user has access to.
|
||||||
|
// This function is called when the user is not an admin.
|
||||||
|
func (kcl *KubeClient) fetchConfigMapsForNonAdmin(namespace string) ([]models.K8sConfigMap, error) {
|
||||||
|
log.Debug().Msgf("Fetching volumes for non-admin user: %v", kcl.NonAdminNamespaces)
|
||||||
|
|
||||||
|
if len(kcl.NonAdminNamespaces) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
configMaps, err := kcl.fetchConfigMaps(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := make([]models.K8sConfigMap, 0)
|
||||||
|
for _, configMap := range configMaps {
|
||||||
|
if _, ok := nonAdminNamespaceSet[configMap.Namespace]; ok {
|
||||||
|
results = append(results, configMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchConfigMaps gets all the ConfigMaps for a given namespace in a k8s endpoint.
|
||||||
|
// the result is a list of config maps parsed into a K8sConfigMap struct.
|
||||||
|
func (kcl *KubeClient) fetchConfigMaps(namespace string) ([]models.K8sConfigMap, error) {
|
||||||
|
configMaps, err := kcl.cli.CoreV1().ConfigMaps(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []models.K8sConfigMap{}
|
||||||
|
for _, configMap := range configMaps.Items {
|
||||||
|
results = append(results, parseConfigMap(&configMap, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kcl *KubeClient) GetConfigMap(namespace, configMapName string) (models.K8sConfigMap, error) {
|
||||||
|
configMap, err := kcl.cli.CoreV1().ConfigMaps(namespace).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return models.K8sConfigMap{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseConfigMap(configMap, true), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseConfigMap parses a k8s ConfigMap object into a K8sConfigMap struct.
|
||||||
|
// for get operation, withData will be set to true.
|
||||||
|
// otherwise, only metadata will be parsed.
|
||||||
|
func parseConfigMap(configMap *corev1.ConfigMap, withData bool) models.K8sConfigMap {
|
||||||
|
result := models.K8sConfigMap{
|
||||||
|
K8sConfiguration: models.K8sConfiguration{
|
||||||
|
UID: string(configMap.UID),
|
||||||
|
Name: configMap.Name,
|
||||||
|
Namespace: configMap.Namespace,
|
||||||
|
CreationDate: configMap.CreationTimestamp.Time.UTC().Format(time.RFC3339),
|
||||||
|
Annotations: configMap.Annotations,
|
||||||
|
Labels: configMap.Labels,
|
||||||
|
ConfigurationOwner: configMap.Labels[labelPortainerKubeConfigOwner],
|
||||||
|
ConfigurationOwnerId: configMap.Labels[labelPortainerKubeConfigOwnerId],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if withData {
|
||||||
|
result.Data = configMap.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombineConfigMapsWithApplications combines the config maps with the applications that use them.
|
||||||
|
// the function fetches all the pods and replica sets in the cluster and checks if the config map is used by any of the pods.
|
||||||
|
// if the config map is used by a pod, the application that uses the pod is added to the config map.
|
||||||
|
// otherwise, the config map is returned as is.
|
||||||
|
func (kcl *KubeClient) CombineConfigMapsWithApplications(configMaps []models.K8sConfigMap) ([]models.K8sConfigMap, error) {
|
||||||
|
updatedConfigMaps := make([]models.K8sConfigMap, len(configMaps))
|
||||||
|
|
||||||
|
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("an error occurred during the CombineConfigMapsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, configMap := range configMaps {
|
||||||
|
updatedConfigMap := configMap
|
||||||
|
|
||||||
|
applicationConfigurationOwners, err := kcl.GetApplicationConfigurationOwnersFromConfigMap(configMap, pods, replicaSets)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("an error occurred during the CombineConfigMapsWithApplications operation, unable to get applications from config map. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(applicationConfigurationOwners) > 0 {
|
||||||
|
updatedConfigMap.ConfigurationOwnerResources = applicationConfigurationOwners
|
||||||
|
updatedConfigMap.IsUsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedConfigMaps[index] = updatedConfigMap
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedConfigMaps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombineConfigMapWithApplications combines the config map with the applications that use it.
|
||||||
|
// the function fetches all the pods in the cluster and checks if the config map is used by any of the pods.
|
||||||
|
// it needs to check if the pods are owned by a replica set to determine if the pod is part of a deployment.
|
||||||
|
func (kcl *KubeClient) CombineConfigMapWithApplications(configMap models.K8sConfigMap) (models.K8sConfigMap, error) {
|
||||||
|
pods, err := kcl.cli.CoreV1().Pods(configMap.Namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return models.K8sConfigMap{}, fmt.Errorf("an error occurred during the CombineConfigMapWithApplications operation, unable to get pods. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
containsReplicaSetOwner := false
|
||||||
|
for _, pod := range pods.Items {
|
||||||
|
containsReplicaSetOwner = isReplicaSetOwner(pod)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if containsReplicaSetOwner {
|
||||||
|
replicaSets, err := kcl.cli.AppsV1().ReplicaSets(configMap.Namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return models.K8sConfigMap{}, fmt.Errorf("an error occurred during the CombineConfigMapWithApplications operation, unable to get replica sets. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationConfigurationOwners, err := kcl.GetApplicationConfigurationOwnersFromConfigMap(configMap, pods.Items, replicaSets.Items)
|
||||||
|
if err != nil {
|
||||||
|
return models.K8sConfigMap{}, fmt.Errorf("an error occurred during the CombineConfigMapWithApplications operation, unable to get applications from config map. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(applicationConfigurationOwners) > 0 {
|
||||||
|
configMap.ConfigurationOwnerResources = applicationConfigurationOwners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configMap, nil
|
||||||
|
}
|
|
@ -1,64 +0,0 @@
|
||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetConfigMapsAndSecrets gets all the ConfigMaps AND all the Secrets for a
|
|
||||||
// given namespace in a k8s endpoint. The result is a list of both config maps
|
|
||||||
// and secrets. The IsSecret boolean property indicates if a given struct is a
|
|
||||||
// secret or configmap.
|
|
||||||
func (kcl *KubeClient) GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) {
|
|
||||||
mapsClient := kcl.cli.CoreV1().ConfigMaps(namespace)
|
|
||||||
mapsList, err := mapsClient.List(context.Background(), v1.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Applications
|
|
||||||
var combined []models.K8sConfigMapOrSecret
|
|
||||||
for _, m := range mapsList.Items {
|
|
||||||
var cm models.K8sConfigMapOrSecret
|
|
||||||
cm.UID = string(m.UID)
|
|
||||||
cm.Name = m.Name
|
|
||||||
cm.Namespace = m.Namespace
|
|
||||||
cm.Annotations = m.Annotations
|
|
||||||
cm.Data = m.Data
|
|
||||||
cm.CreationDate = m.CreationTimestamp.Time.UTC().Format(time.RFC3339)
|
|
||||||
cm.IsSecret = false
|
|
||||||
combined = append(combined, cm)
|
|
||||||
}
|
|
||||||
|
|
||||||
secretClient := kcl.cli.CoreV1().Secrets(namespace)
|
|
||||||
secretList, err := secretClient.List(context.Background(), v1.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range secretList.Items {
|
|
||||||
var secret models.K8sConfigMapOrSecret
|
|
||||||
secret.UID = string(s.UID)
|
|
||||||
secret.Name = s.Name
|
|
||||||
secret.Namespace = s.Namespace
|
|
||||||
secret.Annotations = s.Annotations
|
|
||||||
secret.Data = msbToMss(s.Data)
|
|
||||||
secret.CreationDate = s.CreationTimestamp.Time.UTC().Format(time.RFC3339)
|
|
||||||
secret.IsSecret = true
|
|
||||||
secret.SecretType = string(s.Type)
|
|
||||||
combined = append(combined, secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
return combined, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func msbToMss(msa map[string][]byte) map[string]string {
|
|
||||||
mss := make(map[string]string, len(msa))
|
|
||||||
for k, v := range msa {
|
|
||||||
mss[k] = string(v)
|
|
||||||
}
|
|
||||||
return mss
|
|
||||||
}
|
|
|
@ -151,12 +151,16 @@ func getApplicationsCount(ctx context.Context, kcl *KubeClient, namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
// + (naked pods)
|
// + (naked pods)
|
||||||
nakedPods, err := kcl.GetApplications(namespace, "nakedpods")
|
// TODO: Implement fetching of naked pods
|
||||||
if err != nil {
|
// This is to be reworked as part of the dashboard refactor
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return count + int64(len(nakedPods)), nil
|
// nakedPods, err := kcl.GetApplications(namespace, "nakedpods")
|
||||||
|
// if err != nil {
|
||||||
|
// return 0, err
|
||||||
|
// }
|
||||||
|
// For now, we're not including naked pods in the count
|
||||||
|
|
||||||
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the total count of services for the given namespace
|
// Get the total count of services for the given namespace
|
||||||
|
|
|
@ -2,133 +2,183 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
netv1 "k8s.io/api/networking/v1"
|
netv1 "k8s.io/api/networking/v1"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) {
|
func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) {
|
||||||
var controllers []models.K8sIngressController
|
classeses, err := kcl.cli.NetworkingV1().IngressClasses().List(context.Background(), metav1.ListOptions{})
|
||||||
|
|
||||||
// We know that each existing class points to a controller so we can start
|
|
||||||
// by collecting these easy ones.
|
|
||||||
classClient := kcl.cli.NetworkingV1().IngressClasses()
|
|
||||||
classList, err := classClient.List(context.Background(), metav1.ListOptions{})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to know which of these controllers is in use.
|
ingresses, err := kcl.GetIngresses("")
|
||||||
var ingresses []models.K8sIngressInfo
|
|
||||||
namespaces, err := kcl.GetNamespaces()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for namespace := range namespaces {
|
|
||||||
t, err := kcl.GetIngresses(namespace)
|
|
||||||
if err != 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...)
|
|
||||||
}
|
|
||||||
usedClasses := make(map[string]struct{})
|
usedClasses := make(map[string]struct{})
|
||||||
for _, ingress := range ingresses {
|
for _, ingress := range ingresses {
|
||||||
usedClasses[ingress.ClassName] = struct{}{}
|
usedClasses[ingress.ClassName] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, class := range classList.Items {
|
results := []models.K8sIngressController{}
|
||||||
var controller models.K8sIngressController
|
for _, class := range classeses.Items {
|
||||||
controller.Name = class.Spec.Controller
|
ingressClass := parseIngressClass(class)
|
||||||
controller.ClassName = class.Name
|
|
||||||
|
|
||||||
// If the class is used mark it as such.
|
|
||||||
if _, ok := usedClasses[class.Name]; ok {
|
if _, ok := usedClasses[class.Name]; ok {
|
||||||
controller.Used = true
|
ingressClass.Used = true
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
results = append(results, ingressClass)
|
||||||
case strings.Contains(controller.Name, "nginx"):
|
|
||||||
controller.Type = "nginx"
|
|
||||||
case strings.Contains(controller.Name, "traefik"):
|
|
||||||
controller.Type = "traefik"
|
|
||||||
default:
|
|
||||||
controller.Type = "other"
|
|
||||||
}
|
}
|
||||||
controllers = append(controllers, controller)
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchIngressClasses fetches all the ingress classes in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) fetchIngressClasses() ([]models.K8sIngressController, error) {
|
||||||
|
ingressClasses, err := kcl.cli.NetworkingV1().IngressClasses().List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var controllers []models.K8sIngressController
|
||||||
|
for _, ingressClass := range ingressClasses.Items {
|
||||||
|
controllers = append(controllers, parseIngressClass(ingressClass))
|
||||||
}
|
}
|
||||||
return controllers, nil
|
return controllers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseIngressClass converts a k8s native ingress class object to a Portainer K8sIngressController object.
|
||||||
|
func parseIngressClass(ingressClasses netv1.IngressClass) models.K8sIngressController {
|
||||||
|
ingressContoller := models.K8sIngressController{
|
||||||
|
Name: ingressClasses.Spec.Controller,
|
||||||
|
ClassName: ingressClasses.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(ingressContoller.Name, "nginx"):
|
||||||
|
ingressContoller.Type = "nginx"
|
||||||
|
case strings.Contains(ingressContoller.Name, "traefik"):
|
||||||
|
ingressContoller.Type = "traefik"
|
||||||
|
default:
|
||||||
|
ingressContoller.Type = "other"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ingressContoller
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIngress gets an ingress in a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) GetIngress(namespace, ingressName string) (models.K8sIngressInfo, error) {
|
||||||
|
ingress, err := kcl.cli.NetworkingV1().Ingresses(namespace).Get(context.Background(), ingressName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return models.K8sIngressInfo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseIngress(*ingress), 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.
|
||||||
func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, error) {
|
func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, error) {
|
||||||
// Fetch ingress classes to build a map. We will later use the map to lookup
|
if kcl.IsKubeAdmin {
|
||||||
// each ingresses "type".
|
return kcl.fetchIngresses(namespace)
|
||||||
classes := make(map[string]string)
|
}
|
||||||
classClient := kcl.cli.NetworkingV1().IngressClasses()
|
return kcl.fetchIngressesForNonAdmin(namespace)
|
||||||
classList, err := classClient.List(context.Background(), metav1.ListOptions{})
|
}
|
||||||
|
|
||||||
|
// fetchIngressesForNonAdmin gets all the ingresses for non-admin users in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) fetchIngressesForNonAdmin(namespace string) ([]models.K8sIngressInfo, error) {
|
||||||
|
log.Debug().Msgf("Fetching ingresses for non-admin user: %v", kcl.NonAdminNamespaces)
|
||||||
|
|
||||||
|
if len(kcl.NonAdminNamespaces) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ingresses, err := kcl.fetchIngresses(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, class := range classList.Items {
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
// Write the ingress classes "type" to our map.
|
results := make([]models.K8sIngressInfo, 0)
|
||||||
classes[class.Name] = class.Spec.Controller
|
for _, ingress := range ingresses {
|
||||||
|
if _, ok := nonAdminNamespaceSet[ingress.Namespace]; ok {
|
||||||
|
results = append(results, ingress)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch each ingress.
|
return results, nil
|
||||||
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
}
|
||||||
ingressList, err := ingressClient.List(context.Background(), metav1.ListOptions{})
|
|
||||||
|
// fetchIngresses fetches all the ingresses for a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) fetchIngresses(namespace string) ([]models.K8sIngressInfo, error) {
|
||||||
|
ingresses, err := kcl.cli.NetworkingV1().Ingresses(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var infos []models.K8sIngressInfo
|
ingressClasses, err := kcl.fetchIngressClasses()
|
||||||
for _, ingress := range ingressList.Items {
|
if err != nil {
|
||||||
var info models.K8sIngressInfo
|
return nil, err
|
||||||
info.Name = ingress.Name
|
}
|
||||||
info.UID = string(ingress.UID)
|
|
||||||
info.Namespace = namespace
|
results := []models.K8sIngressInfo{}
|
||||||
ingressClass := ingress.Spec.IngressClassName
|
if len(ingresses.Items) == 0 {
|
||||||
info.ClassName = ""
|
return results, nil
|
||||||
if ingressClass != nil {
|
}
|
||||||
info.ClassName = *ingressClass
|
|
||||||
}
|
for _, ingress := range ingresses.Items {
|
||||||
info.Type = classes[info.ClassName]
|
result := parseIngress(ingress)
|
||||||
info.Annotations = ingress.Annotations
|
if ingress.Spec.IngressClassName != nil {
|
||||||
info.Labels = ingress.Labels
|
result.Type = findUsedIngressFromIngressClasses(ingressClasses, *ingress.Spec.IngressClassName).Name
|
||||||
info.CreationDate = ingress.CreationTimestamp.Time
|
}
|
||||||
|
results = append(results, result)
|
||||||
// Gather TLS information.
|
}
|
||||||
for _, v := range ingress.Spec.TLS {
|
|
||||||
var tls models.K8sIngressTLS
|
return results, nil
|
||||||
tls.Hosts = v.Hosts
|
}
|
||||||
tls.SecretName = v.SecretName
|
|
||||||
info.TLS = append(info.TLS, tls)
|
// parseIngress converts a k8s native ingress object to a Portainer K8sIngressInfo object.
|
||||||
|
func parseIngress(ingress netv1.Ingress) models.K8sIngressInfo {
|
||||||
|
ingressClassName := ""
|
||||||
|
if ingress.Spec.IngressClassName != nil {
|
||||||
|
ingressClassName = *ingress.Spec.IngressClassName
|
||||||
|
}
|
||||||
|
|
||||||
|
result := models.K8sIngressInfo{
|
||||||
|
Name: ingress.Name,
|
||||||
|
Namespace: ingress.Namespace,
|
||||||
|
UID: string(ingress.UID),
|
||||||
|
Annotations: ingress.Annotations,
|
||||||
|
Labels: ingress.Labels,
|
||||||
|
CreationDate: ingress.CreationTimestamp.Time,
|
||||||
|
ClassName: ingressClassName,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tls := range ingress.Spec.TLS {
|
||||||
|
result.TLS = append(result.TLS, models.K8sIngressTLS{
|
||||||
|
Hosts: tls.Hosts,
|
||||||
|
SecretName: tls.SecretName,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather list of paths and hosts.
|
|
||||||
hosts := make(map[string]struct{})
|
hosts := make(map[string]struct{})
|
||||||
for _, r := range ingress.Spec.Rules {
|
for _, r := range ingress.Spec.Rules {
|
||||||
// We collect all exiting hosts in a map to avoid duplicates.
|
|
||||||
// Then, later convert it to a slice for the frontend.
|
|
||||||
hosts[r.Host] = struct{}{}
|
hosts[r.Host] = struct{}{}
|
||||||
|
|
||||||
if r.HTTP == nil {
|
if r.HTTP == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// There are multiple paths per rule. We want to flatten the list
|
|
||||||
// for our frontend.
|
|
||||||
for _, p := range r.HTTP.Paths {
|
for _, p := range r.HTTP.Paths {
|
||||||
var path models.K8sIngressPath
|
var path models.K8sIngressPath
|
||||||
path.IngressName = info.Name
|
path.IngressName = result.Name
|
||||||
path.Host = r.Host
|
path.Host = r.Host
|
||||||
|
|
||||||
path.Path = p.Path
|
path.Path = p.Path
|
||||||
|
@ -137,48 +187,67 @@ func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo,
|
||||||
}
|
}
|
||||||
path.ServiceName = p.Backend.Service.Name
|
path.ServiceName = p.Backend.Service.Name
|
||||||
path.Port = int(p.Backend.Service.Port.Number)
|
path.Port = int(p.Backend.Service.Port.Number)
|
||||||
info.Paths = append(info.Paths, path)
|
result.Paths = append(result.Paths, path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store list of hosts.
|
|
||||||
for host := range hosts {
|
for host := range hosts {
|
||||||
info.Hosts = append(info.Hosts, host)
|
result.Hosts = append(result.Hosts, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
infos = append(infos, info)
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// findUsedIngressFromIngressClasses searches for an ingress in a slice of ingress classes and returns the ingress if found.
|
||||||
|
func findUsedIngressFromIngressClasses(ingressClasses []models.K8sIngressController, className string) models.K8sIngressController {
|
||||||
|
for _, ingressClass := range ingressClasses {
|
||||||
|
if ingressClass.ClassName == className {
|
||||||
|
return ingressClass
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return infos, nil
|
return models.K8sIngressController{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateIngress creates a new ingress in a given namespace in a k8s endpoint.
|
// CreateIngress creates a new ingress in a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error {
|
func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error {
|
||||||
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
ingress := kcl.convertToK8sIngress(info, owner)
|
||||||
var ingress netv1.Ingress
|
_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Create(context.Background(), &ingress, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
ingress.Name = info.Name
|
return err
|
||||||
ingress.Namespace = info.Namespace
|
|
||||||
if info.ClassName != "" {
|
|
||||||
ingress.Spec.IngressClassName = &info.ClassName
|
|
||||||
}
|
}
|
||||||
ingress.Annotations = info.Annotations
|
|
||||||
if ingress.Labels == nil {
|
|
||||||
ingress.Labels = make(map[string]string)
|
|
||||||
}
|
|
||||||
ingress.Labels["io.portainer.kubernetes.ingress.owner"] = stackutils.SanitizeLabel(owner)
|
|
||||||
|
|
||||||
// Store TLS information.
|
return nil
|
||||||
var tls []netv1.IngressTLS
|
}
|
||||||
for _, i := range info.TLS {
|
|
||||||
|
// convertToK8sIngress converts a Portainer K8sIngressInfo object to a k8s native Ingress object.
|
||||||
|
// this is required for create and update operations.
|
||||||
|
func (kcl *KubeClient) convertToK8sIngress(info models.K8sIngressInfo, owner string) netv1.Ingress {
|
||||||
|
result := netv1.Ingress{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: info.Name,
|
||||||
|
Namespace: info.Namespace,
|
||||||
|
Annotations: info.Annotations,
|
||||||
|
},
|
||||||
|
|
||||||
|
Spec: netv1.IngressSpec{
|
||||||
|
IngressClassName: &info.ClassName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := make(map[string]string)
|
||||||
|
labels["io.portainer.kubernetes.ingress.owner"] = stackutils.SanitizeLabel(owner)
|
||||||
|
result.Labels = labels
|
||||||
|
|
||||||
|
tls := []netv1.IngressTLS{}
|
||||||
|
for _, t := range info.TLS {
|
||||||
tls = append(tls, netv1.IngressTLS{
|
tls = append(tls, netv1.IngressTLS{
|
||||||
Hosts: i.Hosts,
|
Hosts: t.Hosts,
|
||||||
SecretName: i.SecretName,
|
SecretName: t.SecretName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ingress.Spec.TLS = tls
|
result.Spec.TLS = tls
|
||||||
|
|
||||||
// Parse "paths" into rules with paths.
|
|
||||||
rules := make(map[string][]netv1.HTTPIngressPath)
|
rules := make(map[string][]netv1.HTTPIngressPath)
|
||||||
for _, path := range info.Paths {
|
for _, path := range info.Paths {
|
||||||
pathType := netv1.PathType(path.PathType)
|
pathType := netv1.PathType(path.PathType)
|
||||||
|
@ -197,7 +266,7 @@ func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInf
|
||||||
}
|
}
|
||||||
|
|
||||||
for rule, paths := range rules {
|
for rule, paths := range rules {
|
||||||
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
|
result.Spec.Rules = append(result.Spec.Rules, netv1.IngressRule{
|
||||||
Host: rule,
|
Host: rule,
|
||||||
IngressRuleValue: netv1.IngressRuleValue{
|
IngressRuleValue: netv1.IngressRuleValue{
|
||||||
HTTP: &netv1.HTTPIngressRuleValue{
|
HTTP: &netv1.HTTPIngressRuleValue{
|
||||||
|
@ -207,102 +276,86 @@ func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInf
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add rules for hosts that does not have paths.
|
|
||||||
// e.g. dafault ingress rule without path to support what we had in 2.15
|
|
||||||
for _, host := range info.Hosts {
|
for _, host := range info.Hosts {
|
||||||
if _, ok := rules[host]; !ok {
|
if _, ok := rules[host]; !ok {
|
||||||
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
|
result.Spec.Rules = append(result.Spec.Rules, netv1.IngressRule{
|
||||||
Host: host,
|
Host: host,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := ingressClient.Create(context.Background(), &ingress, metav1.CreateOptions{})
|
return result
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteIngresses processes a K8sIngressDeleteRequest by deleting each ingress
|
// DeleteIngresses processes a K8sIngressDeleteRequest by deleting each ingress
|
||||||
// in its given namespace.
|
// in its given namespace.
|
||||||
func (kcl *KubeClient) DeleteIngresses(reqs models.K8sIngressDeleteRequests) error {
|
func (kcl *KubeClient) DeleteIngresses(reqs models.K8sIngressDeleteRequests) error {
|
||||||
var err error
|
|
||||||
for namespace := range reqs {
|
for namespace := range reqs {
|
||||||
for _, ingress := range reqs[namespace] {
|
for _, ingress := range reqs[namespace] {
|
||||||
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
err := kcl.cli.NetworkingV1().Ingresses(namespace).Delete(
|
||||||
err = ingressClient.Delete(
|
|
||||||
context.Background(),
|
context.Background(),
|
||||||
ingress,
|
ingress,
|
||||||
metav1.DeleteOptions{},
|
metav1.DeleteOptions{},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateIngress updates an existing ingress in a given namespace in a k8s endpoint.
|
// UpdateIngress updates an existing ingress in a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInfo) error {
|
func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInfo) error {
|
||||||
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
ingress := kcl.convertToK8sIngress(info, "")
|
||||||
ingress, err := ingressClient.Get(context.Background(), info.Name, metav1.GetOptions{})
|
_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Update(context.Background(), &ingress, metav1.UpdateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ingress.Name = info.Name
|
return nil
|
||||||
ingress.Namespace = info.Namespace
|
}
|
||||||
if info.ClassName != "" {
|
|
||||||
ingress.Spec.IngressClassName = &info.ClassName
|
// CombineIngressWithService combines an ingress with a service that is being used by the ingress.
|
||||||
}
|
// this is required to display the service that is being used by the ingress in the UI edit view.
|
||||||
ingress.Annotations = info.Annotations
|
func (kcl *KubeClient) CombineIngressWithService(ingress models.K8sIngressInfo) (models.K8sIngressInfo, error) {
|
||||||
|
services, err := kcl.GetServices(ingress.Namespace)
|
||||||
// Store TLS information.
|
if err != nil {
|
||||||
var tls []netv1.IngressTLS
|
return models.K8sIngressInfo{}, fmt.Errorf("an error occurred during the CombineIngressWithService operation, unable to retrieve services from the Kubernetes for a namespace level user. Error: %w", err)
|
||||||
for _, i := range info.TLS {
|
}
|
||||||
tls = append(tls, netv1.IngressTLS{
|
|
||||||
Hosts: i.Hosts,
|
serviceMap := kcl.buildServicesMap(services)
|
||||||
SecretName: i.SecretName,
|
for pathIndex, path := range ingress.Paths {
|
||||||
})
|
if _, ok := serviceMap[path.ServiceName]; ok {
|
||||||
}
|
ingress.Paths[pathIndex].HasService = true
|
||||||
ingress.Spec.TLS = tls
|
}
|
||||||
|
}
|
||||||
// Parse "paths" into rules with paths.
|
|
||||||
rules := make(map[string][]netv1.HTTPIngressPath)
|
return ingress, nil
|
||||||
for _, path := range info.Paths {
|
}
|
||||||
pathType := netv1.PathType(path.PathType)
|
|
||||||
rules[path.Host] = append(rules[path.Host], netv1.HTTPIngressPath{
|
// CombineIngressesWithServices combines a list of ingresses with a list of services that are being used by the ingresses.
|
||||||
Path: path.Path,
|
// this is required to display the services that are being used by the ingresses in the UI list view.
|
||||||
PathType: &pathType,
|
func (kcl *KubeClient) CombineIngressesWithServices(ingresses []models.K8sIngressInfo) ([]models.K8sIngressInfo, error) {
|
||||||
Backend: netv1.IngressBackend{
|
services, err := kcl.GetServices("")
|
||||||
Service: &netv1.IngressServiceBackend{
|
if err != nil {
|
||||||
Name: path.ServiceName,
|
if k8serrors.IsUnauthorized(err) {
|
||||||
Port: netv1.ServiceBackendPort{
|
return nil, fmt.Errorf("an error occurred during the CombineIngressesWithServices operation, unauthorized access to the Kubernetes API. Error: %w", err)
|
||||||
Number: int32(path.Port),
|
}
|
||||||
},
|
|
||||||
},
|
return nil, fmt.Errorf("an error occurred during the CombineIngressesWithServices operation, unable to retrieve services from the Kubernetes for a cluster level user. Error: %w", err)
|
||||||
},
|
}
|
||||||
})
|
|
||||||
}
|
serviceMap := kcl.buildServicesMap(services)
|
||||||
|
for ingressIndex, ingress := range ingresses {
|
||||||
ingress.Spec.Rules = make([]netv1.IngressRule, 0)
|
for pathIndex, path := range ingress.Paths {
|
||||||
for rule, paths := range rules {
|
if _, ok := serviceMap[path.ServiceName]; ok {
|
||||||
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
|
(ingresses)[ingressIndex].Paths[pathIndex].HasService = true
|
||||||
Host: rule,
|
}
|
||||||
IngressRuleValue: netv1.IngressRuleValue{
|
}
|
||||||
HTTP: &netv1.HTTPIngressRuleValue{
|
}
|
||||||
Paths: paths,
|
|
||||||
},
|
return ingresses, nil
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add rules for hosts that does not have paths.
|
|
||||||
// e.g. dafault ingress rule without path to support what we had in 2.15
|
|
||||||
for _, host := range info.Hosts {
|
|
||||||
if _, ok := rules[host]; !ok {
|
|
||||||
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
|
|
||||||
Host: host,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = ingressClient.Update(context.Background(), ingress, metav1.UpdateOptions{})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,20 +3,27 @@ package cli
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
systemNamespaceLabel = "io.portainer.kubernetes.namespace.system"
|
systemNamespaceLabel = "io.portainer.kubernetes.namespace.system"
|
||||||
|
namespaceOwnerLabel = "io.portainer.kubernetes.resourcepool.owner"
|
||||||
|
namespaceNameLabel = "io.portainer.kubernetes.resourcepool.name"
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultSystemNamespaces() map[string]struct{} {
|
func defaultSystemNamespaces() map[string]struct{} {
|
||||||
|
@ -29,24 +36,69 @@ func defaultSystemNamespaces() map[string]struct{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNamespaces gets the namespaces in the current k8s environment(endpoint).
|
// GetNamespaces gets the namespaces in the current k8s environment(endpoint).
|
||||||
|
// if the user is an admin, all namespaces in the current k8s environment(endpoint) are fetched using the fetchNamespaces function.
|
||||||
|
// otherwise, namespaces the non-admin user has access to will be used to filter the namespaces based on the allowed namespaces.
|
||||||
func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, error) {
|
func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, error) {
|
||||||
namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
|
if kcl.IsKubeAdmin {
|
||||||
if err != nil {
|
return kcl.fetchNamespaces()
|
||||||
return nil, err
|
}
|
||||||
|
return kcl.fetchNamespacesForNonAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchNamespacesForNonAdmin gets the namespaces in the current k8s environment(endpoint) for the non-admin user.
|
||||||
|
func (kcl *KubeClient) fetchNamespacesForNonAdmin() (map[string]portainer.K8sNamespaceInfo, error) {
|
||||||
|
log.Debug().Msgf("Fetching namespaces for non-admin user: %v", kcl.NonAdminNamespaces)
|
||||||
|
|
||||||
|
if len(kcl.NonAdminNamespaces) == 0 {
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
results := make(map[string]portainer.K8sNamespaceInfo)
|
namespaces, err := kcl.fetchNamespaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("an error occurred during the fetchNamespacesForNonAdmin operation, unable to list namespaces for the non-admin user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, ns := range namespaces.Items {
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
results[ns.Name] = portainer.K8sNamespaceInfo{
|
results := make(map[string]portainer.K8sNamespaceInfo)
|
||||||
IsSystem: isSystemNamespace(ns),
|
for _, namespace := range namespaces {
|
||||||
IsDefault: ns.Name == defaultNamespace,
|
if _, exists := nonAdminNamespaceSet[namespace.Name]; exists {
|
||||||
|
results[namespace.Name] = namespace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchNamespaces gets the namespaces in the current k8s environment(endpoint).
|
||||||
|
// this function is used by both admin and non-admin users.
|
||||||
|
// the result gets parsed to a map of namespace name to namespace info.
|
||||||
|
func (kcl *KubeClient) fetchNamespaces() (map[string]portainer.K8sNamespaceInfo, error) {
|
||||||
|
namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("an error occurred during the fetchNamespacesForAdmin operation, unable to list namespaces for the admin user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(map[string]portainer.K8sNamespaceInfo)
|
||||||
|
for _, namespace := range namespaces.Items {
|
||||||
|
results[namespace.Name] = parseNamespace(&namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNamespace converts a k8s namespace object to a portainer namespace object.
|
||||||
|
func parseNamespace(namespace *corev1.Namespace) portainer.K8sNamespaceInfo {
|
||||||
|
return portainer.K8sNamespaceInfo{
|
||||||
|
Id: string(namespace.UID),
|
||||||
|
Name: namespace.Name,
|
||||||
|
Status: namespace.Status,
|
||||||
|
CreationDate: namespace.CreationTimestamp.Format(time.RFC3339),
|
||||||
|
NamespaceOwner: namespace.Labels[namespaceOwnerLabel],
|
||||||
|
IsSystem: isSystemNamespace(*namespace),
|
||||||
|
IsDefault: namespace.Name == defaultNamespace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetNamespace gets the namespace in the current k8s environment(endpoint).
|
// GetNamespace gets the namespace in the current k8s environment(endpoint).
|
||||||
func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, error) {
|
func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, error) {
|
||||||
namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{})
|
namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{})
|
||||||
|
@ -54,47 +106,42 @@ func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, er
|
||||||
return portainer.K8sNamespaceInfo{}, err
|
return portainer.K8sNamespaceInfo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result := portainer.K8sNamespaceInfo{
|
return parseNamespace(namespace), nil
|
||||||
IsSystem: isSystemNamespace(*namespace),
|
|
||||||
IsDefault: namespace.Name == defaultNamespace,
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
|
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error {
|
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error) {
|
||||||
portainerLabels := map[string]string{
|
portainerLabels := map[string]string{
|
||||||
"io.portainer.kubernetes.resourcepool.name": stackutils.SanitizeLabel(info.Name),
|
namespaceNameLabel: stackutils.SanitizeLabel(info.Name),
|
||||||
"io.portainer.kubernetes.resourcepool.owner": stackutils.SanitizeLabel(info.Owner),
|
namespaceOwnerLabel: stackutils.SanitizeLabel(info.Owner),
|
||||||
}
|
}
|
||||||
|
|
||||||
var ns v1.Namespace
|
var ns corev1.Namespace
|
||||||
ns.Name = info.Name
|
ns.Name = info.Name
|
||||||
ns.Annotations = info.Annotations
|
ns.Annotations = info.Annotations
|
||||||
ns.Labels = portainerLabels
|
ns.Labels = portainerLabels
|
||||||
|
|
||||||
_, err := kcl.cli.CoreV1().Namespaces().Create(context.Background(), &ns, metav1.CreateOptions{})
|
namespace, err := kcl.cli.CoreV1().Namespaces().Create(context.Background(), &ns, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("Namespace", info.Name).
|
Str("Namespace", info.Name).
|
||||||
Msg("Failed to create the namespace")
|
Msg("Failed to create the namespace")
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.ResourceQuota != nil && info.ResourceQuota.Enabled {
|
if info.ResourceQuota != nil && info.ResourceQuota.Enabled {
|
||||||
log.Info().Msgf("Creating resource quota for namespace %s", info.Name)
|
log.Info().Msgf("Creating resource quota for namespace %s", info.Name)
|
||||||
log.Debug().Msgf("Creating resource quota with details: %+v", info.ResourceQuota)
|
log.Debug().Msgf("Creating resource quota with details: %+v", info.ResourceQuota)
|
||||||
|
|
||||||
resourceQuota := &v1.ResourceQuota{
|
resourceQuota := &corev1.ResourceQuota{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "portainer-rq-" + info.Name,
|
Name: "portainer-rq-" + info.Name,
|
||||||
Namespace: info.Name,
|
Namespace: info.Name,
|
||||||
Labels: portainerLabels,
|
Labels: portainerLabels,
|
||||||
},
|
},
|
||||||
Spec: v1.ResourceQuotaSpec{
|
Spec: corev1.ResourceQuotaSpec{
|
||||||
Hard: v1.ResourceList{},
|
Hard: corev1.ResourceList{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,28 +150,28 @@ func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error {
|
||||||
cpu := resource.MustParse(info.ResourceQuota.CPU)
|
cpu := resource.MustParse(info.ResourceQuota.CPU)
|
||||||
if memory.Value() > 0 {
|
if memory.Value() > 0 {
|
||||||
memQuota := memory
|
memQuota := memory
|
||||||
resourceQuota.Spec.Hard[v1.ResourceLimitsMemory] = memQuota
|
resourceQuota.Spec.Hard[corev1.ResourceLimitsMemory] = memQuota
|
||||||
resourceQuota.Spec.Hard[v1.ResourceRequestsMemory] = memQuota
|
resourceQuota.Spec.Hard[corev1.ResourceRequestsMemory] = memQuota
|
||||||
}
|
}
|
||||||
|
|
||||||
if cpu.Value() > 0 {
|
if cpu.Value() > 0 {
|
||||||
cpuQuota := cpu
|
cpuQuota := cpu
|
||||||
resourceQuota.Spec.Hard[v1.ResourceLimitsCPU] = cpuQuota
|
resourceQuota.Spec.Hard[corev1.ResourceLimitsCPU] = cpuQuota
|
||||||
resourceQuota.Spec.Hard[v1.ResourceRequestsCPU] = cpuQuota
|
resourceQuota.Spec.Hard[corev1.ResourceRequestsCPU] = cpuQuota
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := kcl.cli.CoreV1().ResourceQuotas(info.Name).Create(context.Background(), resourceQuota, metav1.CreateOptions{})
|
_, err := kcl.cli.CoreV1().ResourceQuotas(info.Name).Create(context.Background(), resourceQuota, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msgf("Failed to create resource quota for namespace %s: %s", info.Name, err)
|
log.Error().Msgf("Failed to create resource quota for namespace %s: %s", info.Name, err)
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return namespace, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSystemNamespace(namespace v1.Namespace) bool {
|
func isSystemNamespace(namespace corev1.Namespace) bool {
|
||||||
systemLabelValue, hasSystemLabel := namespace.Labels[systemNamespaceLabel]
|
systemLabelValue, hasSystemLabel := namespace.Labels[systemNamespaceLabel]
|
||||||
if hasSystemLabel {
|
if hasSystemLabel {
|
||||||
return systemLabelValue == "true"
|
return systemLabelValue == "true"
|
||||||
|
@ -176,32 +223,77 @@ func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) er
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateIngress updates an ingress in a given namespace in a k8s endpoint.
|
// UpdateIngress updates an ingress in a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceDetails) error {
|
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error) {
|
||||||
client := kcl.cli.CoreV1().Namespaces()
|
namespace := corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: info.Name,
|
||||||
|
Annotations: info.Annotations,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var ns v1.Namespace
|
return kcl.cli.CoreV1().Namespaces().Update(context.Background(), &namespace, metav1.UpdateOptions{})
|
||||||
ns.Name = info.Name
|
|
||||||
ns.Annotations = info.Annotations
|
|
||||||
|
|
||||||
_, err := client.Update(context.Background(), &ns, metav1.UpdateOptions{})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kcl *KubeClient) DeleteNamespace(namespace string) error {
|
func (kcl *KubeClient) DeleteNamespace(namespaceName string) (*corev1.Namespace, error) {
|
||||||
client := kcl.cli.CoreV1().Namespaces()
|
namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.Background(), namespaceName, metav1.GetOptions{})
|
||||||
namespaces, err := client.List(context.Background(), metav1.ListOptions{})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ns := range namespaces.Items {
|
err = kcl.cli.CoreV1().Namespaces().Delete(context.Background(), namespaceName, metav1.DeleteOptions{})
|
||||||
if ns.Name == namespace {
|
if err != nil {
|
||||||
return client.Delete(
|
return nil, err
|
||||||
context.Background(),
|
|
||||||
namespace,
|
|
||||||
metav1.DeleteOptions{},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return fmt.Errorf("namespace %s not found", namespace)
|
return namespace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombineNamespacesWithResourceQuotas combines namespaces with resource quotas where matching is based on "portainer-rq-"+namespace.Name
|
||||||
|
func (kcl *KubeClient) CombineNamespacesWithResourceQuotas(namespaces map[string]portainer.K8sNamespaceInfo, w http.ResponseWriter) *httperror.HandlerError {
|
||||||
|
resourceQuotas, err := kcl.GetResourceQuotas("")
|
||||||
|
if err != nil && !k8serrors.IsNotFound(err) {
|
||||||
|
return httperror.InternalServerError("an error occurred during the CombineNamespacesWithResourceQuotas operation, unable to retrieve resource quotas from the Kubernetes for an admin user. Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*resourceQuotas) > 0 {
|
||||||
|
return response.JSON(w, kcl.UpdateNamespacesWithResourceQuotas(namespaces, *resourceQuotas))
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, kcl.ConvertNamespaceMapToSlice(namespaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombineNamespaceWithResourceQuota combines a namespace with a resource quota prefixed with "portainer-rq-"+namespace.Name
|
||||||
|
func (kcl *KubeClient) CombineNamespaceWithResourceQuota(namespace portainer.K8sNamespaceInfo, w http.ResponseWriter) *httperror.HandlerError {
|
||||||
|
resourceQuota, err := kcl.GetPortainerResourceQuota(namespace.Name)
|
||||||
|
if err != nil && !k8serrors.IsNotFound(err) {
|
||||||
|
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the CombineNamespaceWithResourceQuota operation, unable to retrieve the resource quota associated with the namespace: %s for a non-admin user. Error: ", namespace.Name), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceQuota != nil {
|
||||||
|
namespace.ResourceQuota = resourceQuota
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildNonAdminNamespacesMap builds a map of non-admin namespaces.
|
||||||
|
// the map is used to filter the namespaces based on the allowed namespaces.
|
||||||
|
func (kcl *KubeClient) buildNonAdminNamespacesMap() map[string]struct{} {
|
||||||
|
nonAdminNamespaceSet := make(map[string]struct{}, len(kcl.NonAdminNamespaces))
|
||||||
|
for _, namespace := range kcl.NonAdminNamespaces {
|
||||||
|
nonAdminNamespaceSet[namespace] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonAdminNamespaceSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertNamespaceMapToSlice converts the namespace map to a slice of namespaces.
|
||||||
|
// this is used to for the API response.
|
||||||
|
func (kcl *KubeClient) ConvertNamespaceMapToSlice(namespaces map[string]portainer.K8sNamespaceInfo) []portainer.K8sNamespaceInfo {
|
||||||
|
namespaceSlice := make([]portainer.K8sNamespaceInfo, 0, len(namespaces))
|
||||||
|
for _, namespace := range namespaces {
|
||||||
|
namespaceSlice = append(namespaceSlice, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespaceSlice
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,8 +73,18 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||||
t.Run("for regular namespace if isSystem is true and doesn't have a label, should set the label to true", func(t *testing.T) {
|
t.Run("for regular namespace if isSystem is true and doesn't have a label, should set the label to true", func(t *testing.T) {
|
||||||
nsName := "namespace"
|
nsName := "namespace"
|
||||||
|
|
||||||
|
config := &core.ConfigMap{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: portainerConfigMapName,
|
||||||
|
Namespace: portainerNamespace,
|
||||||
|
},
|
||||||
|
Data: map[string]string{
|
||||||
|
"NamespaceAccessPolicies": `{"ns1":{"UserAccessPolicies":{"2":{"RoleId":0}}}, "ns2":{"UserAccessPolicies":{"2":{"RoleId":0}}}}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
kcl := &KubeClient{
|
kcl := &KubeClient{
|
||||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}, config),
|
||||||
instanceID: "instance",
|
instanceID: "instance",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -9,10 +10,69 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (kcl *KubeClient) GetPods(namespace string) ([]corev1.Pod, error) {
|
||||||
|
pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pods.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isReplicaSetOwner checks if the pod's owner reference is a ReplicaSet
|
||||||
|
func isReplicaSetOwner(pod corev1.Pod) bool {
|
||||||
|
return len(pod.OwnerReferences) > 0 && pod.OwnerReferences[0].Kind == "ReplicaSet"
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateOwnerReferenceToDeployment updates the pod's owner reference to the Deployment if applicable
|
||||||
|
func updateOwnerReferenceToDeployment(pod *corev1.Pod, replicaSets []appsv1.ReplicaSet) {
|
||||||
|
for _, replicaSet := range replicaSets {
|
||||||
|
if pod.OwnerReferences[0].Name == replicaSet.Name {
|
||||||
|
if len(replicaSet.OwnerReferences) > 0 && replicaSet.OwnerReferences[0].Kind == "Deployment" {
|
||||||
|
pod.OwnerReferences[0].Kind = "Deployment"
|
||||||
|
pod.OwnerReferences[0].Name = replicaSet.OwnerReferences[0].Name
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsStatefulSetOwnerReference checks if the pod list contains a pod with a StatefulSet owner reference
|
||||||
|
func containsStatefulSetOwnerReference(pods *corev1.PodList) bool {
|
||||||
|
for _, pod := range pods.Items {
|
||||||
|
if len(pod.OwnerReferences) > 0 && pod.OwnerReferences[0].Kind == "StatefulSet" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsDaemonSetOwnerReference checks if the pod list contains a pod with a DaemonSet owner reference
|
||||||
|
func containsDaemonSetOwnerReference(pods *corev1.PodList) bool {
|
||||||
|
for _, pod := range pods.Items {
|
||||||
|
if len(pod.OwnerReferences) > 0 && pod.OwnerReferences[0].Kind == "DaemonSet" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsReplicaSetOwnerReference checks if the pod list contains a pod with a ReplicaSet owner reference
|
||||||
|
func containsReplicaSetOwnerReference(pods *corev1.PodList) bool {
|
||||||
|
for _, pod := range pods.Items {
|
||||||
|
if len(pod.OwnerReferences) > 0 && pod.OwnerReferences[0].Kind == "ReplicaSet" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// CreateUserShellPod will create a kubectl based shell for the specified user by mounting their respective service account.
|
// CreateUserShellPod will create a kubectl based shell for the specified user by mounting their respective service account.
|
||||||
// The lifecycle of the pod is managed in this function; this entails management of the following pod operations:
|
// The lifecycle of the pod is managed in this function; this entails management of the following pod operations:
|
||||||
// - The shell pod will be scoped to specified service accounts access permissions
|
// - The shell pod will be scoped to specified service accounts access permissions
|
||||||
|
@ -24,7 +84,7 @@ func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountNam
|
||||||
|
|
||||||
podPrefix := userShellPodPrefix(serviceAccountName)
|
podPrefix := userShellPodPrefix(serviceAccountName)
|
||||||
|
|
||||||
podSpec := &v1.Pod{
|
podSpec := &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
GenerateName: podPrefix,
|
GenerateName: podPrefix,
|
||||||
Namespace: portainerNamespace,
|
Namespace: portainerNamespace,
|
||||||
|
@ -32,20 +92,20 @@ func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountNam
|
||||||
"kubernetes.io/pod.type": "kubectl-shell",
|
"kubernetes.io/pod.type": "kubectl-shell",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: v1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
TerminationGracePeriodSeconds: new(int64),
|
TerminationGracePeriodSeconds: new(int64),
|
||||||
ServiceAccountName: serviceAccountName,
|
ServiceAccountName: serviceAccountName,
|
||||||
Containers: []v1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
Name: "kubectl-shell-container",
|
Name: "kubectl-shell-container",
|
||||||
Image: shellPodImage,
|
Image: shellPodImage,
|
||||||
Command: []string{"sleep"},
|
Command: []string{"sleep"},
|
||||||
// Specify sleep time to prevent zombie pods in case portainer process is terminated
|
// Specify sleep time to prevent zombie pods in case portainer process is terminated
|
||||||
Args: []string{maxPodKeepAliveSecondsStr},
|
Args: []string{maxPodKeepAliveSecondsStr},
|
||||||
ImagePullPolicy: v1.PullIfNotPresent,
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
RestartPolicy: v1.RestartPolicyNever,
|
RestartPolicy: corev1.RestartPolicyNever,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +118,7 @@ func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountNam
|
||||||
timeoutCtx, cancelFunc := context.WithTimeout(ctx, 20*time.Second)
|
timeoutCtx, cancelFunc := context.WithTimeout(ctx, 20*time.Second)
|
||||||
defer cancelFunc()
|
defer cancelFunc()
|
||||||
|
|
||||||
if err := kcl.waitForPodStatus(timeoutCtx, v1.PodRunning, shellPod); err != nil {
|
if err := kcl.waitForPodStatus(timeoutCtx, corev1.PodRunning, shellPod); err != nil {
|
||||||
kcl.cli.CoreV1().Pods(portainerNamespace).Delete(context.TODO(), shellPod.Name, metav1.DeleteOptions{})
|
kcl.cli.CoreV1().Pods(portainerNamespace).Delete(context.TODO(), shellPod.Name, metav1.DeleteOptions{})
|
||||||
|
|
||||||
return nil, errors.Wrap(err, "aborting pod creation; error waiting for shell pod ready status")
|
return nil, errors.Wrap(err, "aborting pod creation; error waiting for shell pod ready status")
|
||||||
|
@ -89,7 +149,7 @@ func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountNam
|
||||||
|
|
||||||
// waitForPodStatus will wait until duration d (from now) for a pod to reach defined phase/status.
|
// waitForPodStatus will wait until duration d (from now) for a pod to reach defined phase/status.
|
||||||
// The pod status will be polled at specified delay until the pod reaches ready state.
|
// The pod status will be polled at specified delay until the pod reaches ready state.
|
||||||
func (kcl *KubeClient) waitForPodStatus(ctx context.Context, phase v1.PodPhase, pod *v1.Pod) error {
|
func (kcl *KubeClient) waitForPodStatus(ctx context.Context, phase corev1.PodPhase, pod *corev1.Pod) error {
|
||||||
log.Debug().Str("pod", pod.Name).Msg("waiting for pod ready")
|
log.Debug().Str("pod", pod.Name).Msg("waiting for pod ready")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -110,3 +170,102 @@ func (kcl *KubeClient) waitForPodStatus(ctx context.Context, phase v1.PodPhase,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchAllPodsAndReplicaSets fetches all pods and replica sets across the cluster, i.e. all namespaces
|
||||||
|
func (kcl *KubeClient) fetchAllPodsAndReplicaSets(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, error) {
|
||||||
|
return kcl.fetchResourcesWithOwnerReferences(namespace, podListOptions, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchAllApplicationsListResources fetches all pods, replica sets, stateful sets, and daemon sets across the cluster, i.e. all namespaces
|
||||||
|
// this is required for the applications list view
|
||||||
|
func (kcl *KubeClient) fetchAllApplicationsListResources(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, error) {
|
||||||
|
return kcl.fetchResourcesWithOwnerReferences(namespace, podListOptions, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchResourcesWithOwnerReferences fetches pods and other resources based on owner references
|
||||||
|
func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podListOptions metav1.ListOptions, includeStatefulSets, includeDaemonSets bool) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, error) {
|
||||||
|
pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), podListOptions)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
return nil, nil, nil, nil, nil, nil, nil
|
||||||
|
}
|
||||||
|
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list pods across the cluster: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if replicaSet owner reference exists, fetch the replica sets
|
||||||
|
// this also means that the deployments will be fetched because deployments own replica sets
|
||||||
|
replicaSets := &appsv1.ReplicaSetList{}
|
||||||
|
deployments := &appsv1.DeploymentList{}
|
||||||
|
if containsReplicaSetOwnerReference(pods) {
|
||||||
|
replicaSets, err = kcl.cli.AppsV1().ReplicaSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil && !k8serrors.IsNotFound(err) {
|
||||||
|
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list replica sets across the cluster: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deployments, err = kcl.cli.AppsV1().Deployments(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil && !k8serrors.IsNotFound(err) {
|
||||||
|
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list deployments across the cluster: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statefulSets := &appsv1.StatefulSetList{}
|
||||||
|
if includeStatefulSets && containsStatefulSetOwnerReference(pods) {
|
||||||
|
statefulSets, err = kcl.cli.AppsV1().StatefulSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil && !k8serrors.IsNotFound(err) {
|
||||||
|
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list stateful sets across the cluster: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
daemonSets := &appsv1.DaemonSetList{}
|
||||||
|
if includeDaemonSets && containsDaemonSetOwnerReference(pods) {
|
||||||
|
daemonSets, err = kcl.cli.AppsV1().DaemonSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil && !k8serrors.IsNotFound(err) {
|
||||||
|
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list daemon sets across the cluster: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := kcl.cli.CoreV1().Services(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil && !k8serrors.IsNotFound(err) {
|
||||||
|
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list services across the cluster: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pods.Items, replicaSets.Items, deployments.Items, statefulSets.Items, daemonSets.Items, services.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPodUsingConfigMap checks if a pod is using a specific ConfigMap
|
||||||
|
func isPodUsingConfigMap(pod *corev1.Pod, configMapName string) bool {
|
||||||
|
for _, volume := range pod.Spec.Volumes {
|
||||||
|
if volume.ConfigMap != nil && volume.ConfigMap.Name == configMapName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, container := range pod.Spec.Containers {
|
||||||
|
for _, env := range container.Env {
|
||||||
|
if env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil && env.ValueFrom.ConfigMapKeyRef.Name == configMapName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPodUsingSecret checks if a pod is using a specific Secret
|
||||||
|
func isPodUsingSecret(pod *corev1.Pod, secretName string) bool {
|
||||||
|
for _, volume := range pod.Spec.Volumes {
|
||||||
|
if volume.Secret != nil && volume.Secret.SecretName == secretName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, container := range pod.Spec.Containers {
|
||||||
|
for _, env := range container.Env {
|
||||||
|
if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.Name == secretName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -2,198 +2,17 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/internal/randomstring"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
authv1 "k8s.io/api/authorization/v1"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
authv1types "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
|
||||||
corev1types "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
||||||
rbacv1types "k8s.io/client-go/kubernetes/typed/rbac/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxRetries = 5
|
// IsRBACEnabled checks if RBAC is enabled in the current Kubernetes cluster by listing cluster roles.
|
||||||
|
// if the cluster roles can be listed, RBAC is enabled.
|
||||||
// IsRBACEnabled checks if RBAC is enabled in the cluster by creating a service account, then checking it's access to a resourcequota before and after setting a cluster role and cluster role binding
|
// otherwise, RBAC is not enabled.
|
||||||
func (kcl *KubeClient) IsRBACEnabled() (bool, error) {
|
func (kcl *KubeClient) IsRBACEnabled() (bool, error) {
|
||||||
namespace := "default"
|
_, err := kcl.cli.RbacV1().ClusterRoles().List(context.TODO(), metav1.ListOptions{})
|
||||||
verb := "list"
|
|
||||||
resource := "resourcequotas"
|
|
||||||
|
|
||||||
saClient := kcl.cli.CoreV1().ServiceAccounts(namespace)
|
|
||||||
uniqueString := randomstring.RandomString(4) // Append a unique string to resource names, in case they already exist
|
|
||||||
saName := "portainer-rbac-test-sa-" + uniqueString
|
|
||||||
if err := createServiceAccount(saClient, saName, namespace); err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error creating service account")
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer deleteServiceAccount(saClient, saName)
|
|
||||||
|
|
||||||
accessReviewClient := kcl.cli.AuthorizationV1().LocalSubjectAccessReviews(namespace)
|
|
||||||
allowed, err := checkServiceAccountAccess(accessReviewClient, saName, verb, resource, namespace)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Error checking service account access")
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the service account with no authorizations is allowed, RBAC must be disabled
|
|
||||||
if allowed {
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
return true, nil
|
||||||
// Otherwise give the service account an rbac authorisation and check again
|
|
||||||
roleClient := kcl.cli.RbacV1().Roles(namespace)
|
|
||||||
roleName := "portainer-rbac-test-role-" + uniqueString
|
|
||||||
if err := createRole(roleClient, roleName, verb, resource, namespace); err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error creating role")
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer deleteRole(roleClient, roleName)
|
|
||||||
|
|
||||||
roleBindingClient := kcl.cli.RbacV1().RoleBindings(namespace)
|
|
||||||
roleBindingName := "portainer-rbac-test-role-binding-" + uniqueString
|
|
||||||
if err := createRoleBinding(roleBindingClient, roleBindingName, roleName, saName, namespace); err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error creating role binding")
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer deleteRoleBinding(roleBindingClient, roleBindingName)
|
|
||||||
|
|
||||||
allowed, err = checkServiceAccountAccess(accessReviewClient, saName, verb, resource, namespace)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error checking service account access with authorizations added")
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the service account allowed to list resource quotas after given rbac role, then RBAC is enabled
|
|
||||||
return allowed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createServiceAccount(saClient corev1types.ServiceAccountInterface, name string, namespace string) error {
|
|
||||||
serviceAccount := &corev1.ServiceAccount{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: name,
|
|
||||||
Namespace: namespace,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := saClient.Create(context.Background(), serviceAccount, metav1.CreateOptions{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteServiceAccount(saClient corev1types.ServiceAccountInterface, name string) {
|
|
||||||
if err := saClient.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error deleting service account: " + name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createRole(roleClient rbacv1types.RoleInterface, name string, verb string, resource string, namespace string) error {
|
|
||||||
role := &rbacv1.Role{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: name,
|
|
||||||
Namespace: namespace,
|
|
||||||
},
|
|
||||||
Rules: []rbacv1.PolicyRule{
|
|
||||||
{
|
|
||||||
APIGroups: []string{""},
|
|
||||||
Verbs: []string{verb},
|
|
||||||
Resources: []string{resource},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := roleClient.Create(context.Background(), role, metav1.CreateOptions{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteRole(roleClient rbacv1types.RoleInterface, name string) {
|
|
||||||
if err := roleClient.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error deleting role: " + name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createRoleBinding(roleBindingClient rbacv1types.RoleBindingInterface, clusterRoleBindingName string, roleName string, serviceAccountName string, namespace string) error {
|
|
||||||
clusterRoleBinding := &rbacv1.RoleBinding{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: clusterRoleBindingName,
|
|
||||||
},
|
|
||||||
Subjects: []rbacv1.Subject{
|
|
||||||
{
|
|
||||||
Kind: "ServiceAccount",
|
|
||||||
Name: serviceAccountName,
|
|
||||||
Namespace: namespace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
RoleRef: rbacv1.RoleRef{
|
|
||||||
Kind: "Role",
|
|
||||||
Name: roleName,
|
|
||||||
APIGroup: "rbac.authorization.k8s.io",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
roleBinding, err := roleBindingClient.Create(context.Background(), clusterRoleBinding, metav1.CreateOptions{})
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error creating role binding: " + clusterRoleBindingName)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry checkRoleBinding a maximum of 5 times with a 100ms wait after each attempt
|
|
||||||
for range maxRetries {
|
|
||||||
err = checkRoleBinding(roleBindingClient, roleBinding.Name)
|
|
||||||
time.Sleep(100 * time.Millisecond) // Wait for 100ms, even if the check passes
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkRoleBinding(roleBindingClient rbacv1types.RoleBindingInterface, name string) error {
|
|
||||||
if _, err := roleBindingClient.Get(context.Background(), name, metav1.GetOptions{}); err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error finding rolebinding: " + name)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteRoleBinding(roleBindingClient rbacv1types.RoleBindingInterface, name string) {
|
|
||||||
if err := roleBindingClient.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error deleting role binding: " + name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkServiceAccountAccess(accessReviewClient authv1types.LocalSubjectAccessReviewInterface, serviceAccountName string, verb string, resource string, namespace string) (bool, error) {
|
|
||||||
subjectAccessReview := &authv1.LocalSubjectAccessReview{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: namespace,
|
|
||||||
},
|
|
||||||
Spec: authv1.SubjectAccessReviewSpec{
|
|
||||||
ResourceAttributes: &authv1.ResourceAttributes{
|
|
||||||
Namespace: namespace,
|
|
||||||
Verb: verb,
|
|
||||||
Resource: resource,
|
|
||||||
},
|
|
||||||
User: "system:serviceaccount:default:" + serviceAccountName, // a workaround to be able to use the service account as a user
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := accessReviewClient.Create(context.Background(), subjectAccessReview, metav1.CreateOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Status.Allowed, nil
|
|
||||||
}
|
}
|
||||||
|
|
95
api/kubernetes/cli/resource_quota.go
Normal file
95
api/kubernetes/cli/resource_quota.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetResourceQuotas gets all resource quotas in the current k8s environment(endpoint).
|
||||||
|
// if the user is an admin, all resource quotas in all namespaces are fetched.
|
||||||
|
// otherwise, namespaces the non-admin user has access to will be used to filter the resource quotas.
|
||||||
|
func (kcl *KubeClient) GetResourceQuotas(namespace string) (*[]corev1.ResourceQuota, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchResourceQuotas(namespace)
|
||||||
|
}
|
||||||
|
return kcl.fetchResourceQuotasForNonAdmin(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchResourceQuotasForNonAdmin gets the resource quotas in the current k8s environment(endpoint) for a non-admin user.
|
||||||
|
// the role of the user must have read access to the resource quotas in the defined namespaces.
|
||||||
|
func (kcl *KubeClient) fetchResourceQuotasForNonAdmin(namespace string) (*[]corev1.ResourceQuota, error) {
|
||||||
|
log.Debug().Msgf("Fetching resource quotas for non-admin user: %v", kcl.NonAdminNamespaces)
|
||||||
|
|
||||||
|
if len(kcl.NonAdminNamespaces) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceQuotas, err := kcl.fetchResourceQuotas(namespace)
|
||||||
|
if err != nil && !k8serrors.IsNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := []corev1.ResourceQuota{}
|
||||||
|
for _, resourceQuota := range *resourceQuotas {
|
||||||
|
if _, exists := nonAdminNamespaceSet[resourceQuota.Namespace]; exists {
|
||||||
|
results = append(results, resourceQuota)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kcl *KubeClient) fetchResourceQuotas(namespace string) (*[]corev1.ResourceQuota, error) {
|
||||||
|
resourceQuotas, err := kcl.cli.CoreV1().ResourceQuotas(namespace).List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("an error occured, failed to list resource quotas for the admin user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resourceQuotas.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPortainerResourceQuota gets the resource quota for the portainer namespace.
|
||||||
|
// The resource quota is prefixed with "portainer-rq-".
|
||||||
|
func (kcl *KubeClient) GetPortainerResourceQuota(namespace string) (*corev1.ResourceQuota, error) {
|
||||||
|
return kcl.cli.CoreV1().ResourceQuotas(namespace).Get(context.TODO(), "portainer-rq-"+namespace, metav1.GetOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResourceQuota gets a resource quota in a specific namespace.
|
||||||
|
func (kcl *KubeClient) GetResourceQuota(namespace, resourceQuota string) (*corev1.ResourceQuota, error) {
|
||||||
|
return kcl.cli.CoreV1().ResourceQuotas(namespace).Get(context.TODO(), resourceQuota, metav1.GetOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateNamespacesWithResourceQuotas updates the namespaces with the resource quotas.
|
||||||
|
// The resource quotas are matched with the namespaces by name.
|
||||||
|
func (kcl *KubeClient) UpdateNamespacesWithResourceQuotas(namespaces map[string]portainer.K8sNamespaceInfo, resourceQuotas []corev1.ResourceQuota) []portainer.K8sNamespaceInfo {
|
||||||
|
namespacesWithQuota := map[string]portainer.K8sNamespaceInfo{}
|
||||||
|
|
||||||
|
for _, namespace := range namespaces {
|
||||||
|
resourceQuota := kcl.GetResourceQuotaFromNamespace(namespace, resourceQuotas)
|
||||||
|
if resourceQuota != nil {
|
||||||
|
namespace.ResourceQuota = resourceQuota
|
||||||
|
}
|
||||||
|
|
||||||
|
namespacesWithQuota[namespace.Name] = namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
return kcl.ConvertNamespaceMapToSlice(namespacesWithQuota)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResourceQuotaFromNamespace gets the resource quota in a specific namespace where the resource quota's name is prefixed with "portainer-rq-".
|
||||||
|
func (kcl *KubeClient) GetResourceQuotaFromNamespace(namespace portainer.K8sNamespaceInfo, resourceQuotas []corev1.ResourceQuota) *corev1.ResourceQuota {
|
||||||
|
for _, resourceQuota := range resourceQuotas {
|
||||||
|
if resourceQuota.ObjectMeta.Namespace == namespace.Name && resourceQuota.ObjectMeta.Name == "portainer-rq-"+namespace.Name {
|
||||||
|
return &resourceQuota
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -3,11 +3,66 @@ package cli
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetRoles gets all the roles for either at the cluster level or a given namespace in a k8s endpoint.
|
||||||
|
// It returns a list of K8sRole objects.
|
||||||
|
func (kcl *KubeClient) GetRoles(namespace string) ([]models.K8sRole, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchRoles(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kcl.fetchRolesForNonAdmin(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchRolesForNonAdmin gets all the roles for either at the cluster level or a given namespace in a k8s endpoint.
|
||||||
|
// the namespace will be coming from NonAdminNamespaces as non-admin users are restricted to certain namespaces.
|
||||||
|
// it returns a list of K8sRole objects.
|
||||||
|
func (kcl *KubeClient) fetchRolesForNonAdmin(namespace string) ([]models.K8sRole, error) {
|
||||||
|
roles, err := kcl.fetchRoles(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := make([]models.K8sRole, 0)
|
||||||
|
for _, role := range roles {
|
||||||
|
if _, ok := nonAdminNamespaceSet[role.Namespace]; ok {
|
||||||
|
results = append(results, role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchRoles returns a list of all Roles in the specified namespace.
|
||||||
|
func (kcl *KubeClient) fetchRoles(namespace string) ([]models.K8sRole, error) {
|
||||||
|
roles, err := kcl.cli.RbacV1().Roles(namespace).List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]models.K8sRole, 0)
|
||||||
|
for _, role := range roles.Items {
|
||||||
|
results = append(results, parseRole(role))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseRole converts a rbacv1.Role object to a models.K8sRole object.
|
||||||
|
func parseRole(role rbacv1.Role) models.K8sRole {
|
||||||
|
return models.K8sRole{
|
||||||
|
Name: role.Name,
|
||||||
|
Namespace: role.Namespace,
|
||||||
|
CreationDate: role.CreationTimestamp.Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
|
func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
|
||||||
return []rbacv1.PolicyRule{
|
return []rbacv1.PolicyRule{
|
||||||
{
|
{
|
||||||
|
|
65
api/kubernetes/cli/role_binding.go
Normal file
65
api/kubernetes/cli/role_binding.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRoleBindings gets all the roleBindings for either at the cluster level or a given namespace in a k8s endpoint.
|
||||||
|
// It returns a list of K8sRoleBinding objects.
|
||||||
|
func (kcl *KubeClient) GetRoleBindings(namespace string) ([]models.K8sRoleBinding, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchRoleBindings(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kcl.fetchRolebindingsForNonAdmin(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchRolebindingsForNonAdmin gets all the roleBindings for either at the cluster level or a given namespace in a k8s endpoint.
|
||||||
|
// the namespace will be coming from NonAdminNamespaces as non-admin users are restricted to certain namespaces.
|
||||||
|
// it returns a list of K8sRoleBinding objects.
|
||||||
|
func (kcl *KubeClient) fetchRolebindingsForNonAdmin(namespace string) ([]models.K8sRoleBinding, error) {
|
||||||
|
roleBindings, err := kcl.fetchRoleBindings(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := make([]models.K8sRoleBinding, 0)
|
||||||
|
for _, roleBinding := range roleBindings {
|
||||||
|
if _, ok := nonAdminNamespaceSet[roleBinding.Namespace]; ok {
|
||||||
|
results = append(results, roleBinding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchRoleBindings returns a list of all Roles in the specified namespace.
|
||||||
|
func (kcl *KubeClient) fetchRoleBindings(namespace string) ([]models.K8sRoleBinding, error) {
|
||||||
|
roleBindings, err := kcl.cli.RbacV1().RoleBindings(namespace).List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]models.K8sRoleBinding, 0)
|
||||||
|
for _, roleBinding := range roleBindings.Items {
|
||||||
|
results = append(results, parseRoleBinding(roleBinding))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseRoleBinding converts a rbacv1.RoleBinding object to a models.K8sRoleBinding object.
|
||||||
|
func parseRoleBinding(roleBinding rbacv1.RoleBinding) models.K8sRoleBinding {
|
||||||
|
return models.K8sRoleBinding{
|
||||||
|
Name: roleBinding.Name,
|
||||||
|
Namespace: roleBinding.Namespace,
|
||||||
|
RoleRef: roleBinding.RoleRef,
|
||||||
|
Subjects: roleBinding.Subjects,
|
||||||
|
CreationDate: roleBinding.CreationTimestamp.Time,
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,17 +3,182 @@ package cli
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
labelPortainerKubeConfigOwner = "io.portainer.kubernetes.configuration.owner"
|
||||||
|
labelPortainerKubeConfigOwnerId = "io.portainer.kubernetes.configuration.owner.id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetSecrets gets all the Secrets for a given namespace in a k8s endpoint.
|
||||||
|
// if the user is an admin, all secrets in the current k8s environment(endpoint) are fetched using the getSecrets function.
|
||||||
|
// otherwise, namespaces the non-admin user has access to will be used to filter the secrets based on the allowed namespaces.
|
||||||
|
func (kcl *KubeClient) GetSecrets(namespace string) ([]models.K8sSecret, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.getSecrets(namespace)
|
||||||
|
}
|
||||||
|
return kcl.getSecretsForNonAdmin(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSecretsForNonAdmin fetches the secrets in the namespaces the user has access to.
|
||||||
|
// This function is called when the user is not an admin.
|
||||||
|
func (kcl *KubeClient) getSecretsForNonAdmin(namespace string) ([]models.K8sSecret, error) {
|
||||||
|
log.Debug().Msgf("Fetching volumes for non-admin user: %v", kcl.NonAdminNamespaces)
|
||||||
|
|
||||||
|
if len(kcl.NonAdminNamespaces) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := kcl.getSecrets(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := make([]models.K8sSecret, 0)
|
||||||
|
for _, secret := range secrets {
|
||||||
|
if _, ok := nonAdminNamespaceSet[secret.Namespace]; ok {
|
||||||
|
results = append(results, secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSecrets gets all the Secrets for a given namespace in a k8s endpoint.
|
||||||
|
// the result is a list of secrets parsed into a K8sSecret struct.
|
||||||
|
func (kcl *KubeClient) getSecrets(namespace string) ([]models.K8sSecret, error) {
|
||||||
|
secrets, err := kcl.cli.CoreV1().Secrets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []models.K8sSecret{}
|
||||||
|
for _, secret := range secrets.Items {
|
||||||
|
results = append(results, parseSecret(&secret, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecret gets a Secret by name for a given namespace.
|
||||||
|
// the result is a secret parsed into a K8sSecret struct.
|
||||||
|
func (kcl *KubeClient) GetSecret(namespace string, secretName string) (models.K8sSecret, error) {
|
||||||
|
secret, err := kcl.cli.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return models.K8sSecret{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseSecret(secret, true), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSecret parses a k8s Secret object into a K8sSecret struct.
|
||||||
|
// for get operation, withData will be set to true.
|
||||||
|
// otherwise, only metadata will be parsed.
|
||||||
|
func parseSecret(secret *corev1.Secret, withData bool) models.K8sSecret {
|
||||||
|
result := models.K8sSecret{
|
||||||
|
K8sConfiguration: models.K8sConfiguration{
|
||||||
|
UID: string(secret.UID),
|
||||||
|
Name: secret.Name,
|
||||||
|
Namespace: secret.Namespace,
|
||||||
|
CreationDate: secret.CreationTimestamp.Time.UTC().Format(time.RFC3339),
|
||||||
|
Annotations: secret.Annotations,
|
||||||
|
Labels: secret.Labels,
|
||||||
|
ConfigurationOwner: secret.Labels[labelPortainerKubeConfigOwner],
|
||||||
|
ConfigurationOwnerId: secret.Labels[labelPortainerKubeConfigOwnerId],
|
||||||
|
},
|
||||||
|
SecretType: string(secret.Type),
|
||||||
|
}
|
||||||
|
|
||||||
|
if withData {
|
||||||
|
secretData := secret.Data
|
||||||
|
secretDataMap := make(map[string]string, len(secretData))
|
||||||
|
for key, value := range secretData {
|
||||||
|
secretDataMap[key] = string(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Data = secretDataMap
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombineSecretsWithApplications combines the secrets with the applications that use them.
|
||||||
|
// the function fetches all the pods and replica sets in the cluster and checks if the secret is used by any of the pods.
|
||||||
|
// if the secret is used by a pod, the application that uses the pod is added to the secret.
|
||||||
|
// otherwise, the secret is returned as is.
|
||||||
|
func (kcl *KubeClient) CombineSecretsWithApplications(secrets []models.K8sSecret) ([]models.K8sSecret, error) {
|
||||||
|
updatedSecrets := make([]models.K8sSecret, len(secrets))
|
||||||
|
|
||||||
|
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("an error occurred during the CombineSecretsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, secret := range secrets {
|
||||||
|
updatedSecret := secret
|
||||||
|
|
||||||
|
applicationConfigurationOwners, err := kcl.GetApplicationConfigurationOwnersFromSecret(secret, pods, replicaSets)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("an error occurred during the CombineSecretsWithApplications operation, unable to get applications from secret. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(applicationConfigurationOwners) > 0 {
|
||||||
|
updatedSecret.ConfigurationOwnerResources = applicationConfigurationOwners
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedSecrets[index] = updatedSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedSecrets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombineSecretWithApplications combines the secret with the applications that use it.
|
||||||
|
// the function fetches all the pods in the cluster and checks if the secret is used by any of the pods.
|
||||||
|
// it needs to check if the pods are owned by a replica set to determine if the pod is part of a deployment.
|
||||||
|
func (kcl *KubeClient) CombineSecretWithApplications(secret models.K8sSecret) (models.K8sSecret, error) {
|
||||||
|
pods, err := kcl.cli.CoreV1().Pods(secret.Namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return models.K8sSecret{}, fmt.Errorf("an error occurred during the CombineSecretWithApplications operation, unable to get pods. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
containsReplicaSetOwner := false
|
||||||
|
for _, pod := range pods.Items {
|
||||||
|
containsReplicaSetOwner = isReplicaSetOwner(pod)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if containsReplicaSetOwner {
|
||||||
|
replicaSets, err := kcl.cli.AppsV1().ReplicaSets(secret.Namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return models.K8sSecret{}, fmt.Errorf("an error occurred during the CombineSecretWithApplications operation, unable to get replica sets. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationConfigurationOwners, err := kcl.GetApplicationConfigurationOwnersFromSecret(secret, pods.Items, replicaSets.Items)
|
||||||
|
if err != nil {
|
||||||
|
return models.K8sSecret{}, fmt.Errorf("an error occurred during the CombineSecretWithApplications operation, unable to get applications from secret. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(applicationConfigurationOwners) > 0 {
|
||||||
|
secret.ConfigurationOwnerResources = applicationConfigurationOwners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (kcl *KubeClient) createServiceAccountToken(serviceAccountName string) error {
|
func (kcl *KubeClient) createServiceAccountToken(serviceAccountName string) error {
|
||||||
serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName, kcl.instanceID)
|
serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName, kcl.instanceID)
|
||||||
|
|
||||||
serviceAccountSecret := &v1.Secret{
|
serviceAccountSecret := &corev1.Secret{
|
||||||
TypeMeta: metav1.TypeMeta{},
|
TypeMeta: metav1.TypeMeta{},
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: serviceAccountSecretName,
|
Name: serviceAccountSecretName,
|
||||||
|
|
|
@ -2,27 +2,71 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
labels "k8s.io/apimachinery/pkg/labels"
|
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetServices gets all the services for a given namespace in a k8s endpoint.
|
// GetServices gets all the services for either at the cluster level or a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) GetServices(namespace string, lookupApplications bool) ([]models.K8sServiceInfo, error) {
|
// It returns a list of K8sServiceInfo objects.
|
||||||
client := kcl.cli.CoreV1().Services(namespace)
|
func (kcl *KubeClient) GetServices(namespace string) ([]models.K8sServiceInfo, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchServices(namespace)
|
||||||
|
}
|
||||||
|
return kcl.fetchServicesForNonAdmin(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
services, err := client.List(context.Background(), metav1.ListOptions{})
|
// fetchServicesForNonAdmin gets all the services for either at the cluster level or a given namespace in a k8s endpoint.
|
||||||
|
// the namespace will be coming from NonAdminNamespaces as non-admin users are restricted to certain namespaces.
|
||||||
|
// it returns a list of K8sServiceInfo objects.
|
||||||
|
func (kcl *KubeClient) fetchServicesForNonAdmin(namespace string) ([]models.K8sServiceInfo, error) {
|
||||||
|
log.Debug().Msgf("Fetching services for non-admin user: %v", kcl.NonAdminNamespaces)
|
||||||
|
|
||||||
|
if len(kcl.NonAdminNamespaces) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := kcl.fetchServices(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []models.K8sServiceInfo
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := make([]models.K8sServiceInfo, 0)
|
||||||
|
for _, service := range services {
|
||||||
|
if _, ok := nonAdminNamespaceSet[service.Namespace]; ok {
|
||||||
|
results = append(results, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchServices gets the services in a given namespace in a k8s endpoint.
|
||||||
|
// It returns a list of K8sServiceInfo objects.
|
||||||
|
func (kcl *KubeClient) fetchServices(namespace string) ([]models.K8sServiceInfo, error) {
|
||||||
|
services, err := kcl.cli.CoreV1().Services(namespace).List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]models.K8sServiceInfo, 0)
|
||||||
for _, service := range services.Items {
|
for _, service := range services.Items {
|
||||||
|
results = append(results, parseService(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseService converts a k8s native service object to a Portainer K8sServiceInfo object.
|
||||||
|
// service ports, ingress status, labels, annotations, cluster IPs, and external IPs are parsed.
|
||||||
|
// it returns a K8sServiceInfo object.
|
||||||
|
func parseService(service corev1.Service) models.K8sServiceInfo {
|
||||||
servicePorts := make([]models.K8sServicePort, 0)
|
servicePorts := make([]models.K8sServicePort, 0)
|
||||||
for _, port := range service.Spec.Ports {
|
for _, port := range service.Spec.Ports {
|
||||||
servicePorts = append(servicePorts, models.K8sServicePort{
|
servicePorts = append(servicePorts, models.K8sServicePort{
|
||||||
|
@ -42,17 +86,12 @@ func (kcl *KubeClient) GetServices(namespace string, lookupApplications bool) ([
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var applications []models.K8sApplication
|
return models.K8sServiceInfo{
|
||||||
if lookupApplications {
|
|
||||||
applications, _ = kcl.getOwningApplication(namespace, service.Spec.Selector)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, models.K8sServiceInfo{
|
|
||||||
Name: service.Name,
|
Name: service.Name,
|
||||||
UID: string(service.GetUID()),
|
UID: string(service.GetUID()),
|
||||||
Type: string(service.Spec.Type),
|
Type: string(service.Spec.Type),
|
||||||
Namespace: service.Namespace,
|
Namespace: service.Namespace,
|
||||||
CreationTimestamp: service.GetCreationTimestamp().String(),
|
CreationDate: service.GetCreationTimestamp().String(),
|
||||||
AllocateLoadBalancerNodePorts: service.Spec.AllocateLoadBalancerNodePorts,
|
AllocateLoadBalancerNodePorts: service.Spec.AllocateLoadBalancerNodePorts,
|
||||||
Ports: servicePorts,
|
Ports: servicePorts,
|
||||||
IngressStatus: ingressStatus,
|
IngressStatus: ingressStatus,
|
||||||
|
@ -61,40 +100,37 @@ func (kcl *KubeClient) GetServices(namespace string, lookupApplications bool) ([
|
||||||
ClusterIPs: service.Spec.ClusterIPs,
|
ClusterIPs: service.Spec.ClusterIPs,
|
||||||
ExternalName: service.Spec.ExternalName,
|
ExternalName: service.Spec.ExternalName,
|
||||||
ExternalIPs: service.Spec.ExternalIPs,
|
ExternalIPs: service.Spec.ExternalIPs,
|
||||||
Applications: applications,
|
Selector: service.Spec.Selector,
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kcl *KubeClient) fillService(info models.K8sServiceInfo) v1.Service {
|
// convertToK8sService converts a K8sServiceInfo object back to a k8s native service object.
|
||||||
var service v1.Service
|
// this is required for create and update operations.
|
||||||
|
// it returns a v1.Service object.
|
||||||
|
func (kcl *KubeClient) convertToK8sService(info models.K8sServiceInfo) corev1.Service {
|
||||||
|
service := corev1.Service{}
|
||||||
service.Name = info.Name
|
service.Name = info.Name
|
||||||
service.Spec.Type = v1.ServiceType(info.Type)
|
service.Spec.Type = corev1.ServiceType(info.Type)
|
||||||
service.Namespace = info.Namespace
|
service.Namespace = info.Namespace
|
||||||
service.Annotations = info.Annotations
|
service.Annotations = info.Annotations
|
||||||
service.Labels = info.Labels
|
service.Labels = info.Labels
|
||||||
service.Spec.AllocateLoadBalancerNodePorts = info.AllocateLoadBalancerNodePorts
|
service.Spec.AllocateLoadBalancerNodePorts = info.AllocateLoadBalancerNodePorts
|
||||||
service.Spec.Selector = info.Selector
|
service.Spec.Selector = info.Selector
|
||||||
|
|
||||||
// Set ports.
|
|
||||||
for _, p := range info.Ports {
|
for _, p := range info.Ports {
|
||||||
var port v1.ServicePort
|
port := corev1.ServicePort{}
|
||||||
port.Name = p.Name
|
port.Name = p.Name
|
||||||
port.NodePort = int32(p.NodePort)
|
port.NodePort = int32(p.NodePort)
|
||||||
port.Port = int32(p.Port)
|
port.Port = int32(p.Port)
|
||||||
port.Protocol = v1.Protocol(p.Protocol)
|
port.Protocol = corev1.Protocol(p.Protocol)
|
||||||
port.TargetPort = intstr.FromString(p.TargetPort)
|
port.TargetPort = intstr.FromString(p.TargetPort)
|
||||||
service.Spec.Ports = append(service.Spec.Ports, port)
|
service.Spec.Ports = append(service.Spec.Ports, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set ingresses.
|
|
||||||
for _, i := range info.IngressStatus {
|
for _, i := range info.IngressStatus {
|
||||||
service.Status.LoadBalancer.Ingress = append(
|
service.Status.LoadBalancer.Ingress = append(
|
||||||
service.Status.LoadBalancer.Ingress,
|
service.Status.LoadBalancer.Ingress,
|
||||||
v1.LoadBalancerIngress{IP: i.IP, Hostname: i.Host},
|
corev1.LoadBalancerIngress{IP: i.IP, Hostname: i.Host},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,86 +139,84 @@ func (kcl *KubeClient) fillService(info models.K8sServiceInfo) v1.Service {
|
||||||
|
|
||||||
// CreateService creates a new service in a given namespace in a k8s endpoint.
|
// CreateService creates a new service in a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) CreateService(namespace string, info models.K8sServiceInfo) error {
|
func (kcl *KubeClient) CreateService(namespace string, info models.K8sServiceInfo) error {
|
||||||
serviceClient := kcl.cli.CoreV1().Services(namespace)
|
service := kcl.convertToK8sService(info)
|
||||||
service := kcl.fillService(info)
|
_, err := kcl.cli.CoreV1().Services(namespace).Create(context.Background(), &service, metav1.CreateOptions{})
|
||||||
|
|
||||||
_, err := serviceClient.Create(context.Background(), &service, metav1.CreateOptions{})
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteServices processes a K8sServiceDeleteRequest by deleting each service
|
// DeleteServices processes a K8sServiceDeleteRequest by deleting each service
|
||||||
// in its given namespace.
|
// in its given namespace.
|
||||||
func (kcl *KubeClient) DeleteServices(reqs models.K8sServiceDeleteRequests) error {
|
func (kcl *KubeClient) DeleteServices(reqs models.K8sServiceDeleteRequests) error {
|
||||||
var err error
|
|
||||||
for namespace := range reqs {
|
for namespace := range reqs {
|
||||||
for _, service := range reqs[namespace] {
|
for _, service := range reqs[namespace] {
|
||||||
serviceClient := kcl.cli.CoreV1().Services(namespace)
|
err := kcl.cli.CoreV1().Services(namespace).Delete(context.Background(), service, metav1.DeleteOptions{})
|
||||||
err = serviceClient.Delete(
|
if err != nil {
|
||||||
context.Background(),
|
return err
|
||||||
service,
|
}
|
||||||
metav1.DeleteOptions{},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateService updates service in a given namespace in a k8s endpoint.
|
// UpdateService updates service in a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) UpdateService(namespace string, info models.K8sServiceInfo) error {
|
func (kcl *KubeClient) UpdateService(namespace string, info models.K8sServiceInfo) error {
|
||||||
serviceClient := kcl.cli.CoreV1().Services(namespace)
|
service := kcl.convertToK8sService(info)
|
||||||
service := kcl.fillService(info)
|
_, err := kcl.cli.CoreV1().Services(namespace).Update(context.Background(), &service, metav1.UpdateOptions{})
|
||||||
|
|
||||||
_, err := serviceClient.Update(context.Background(), &service, metav1.UpdateOptions{})
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOwningApplication gets the application that owns the given service selector.
|
// CombineServicesWithApplications retrieves applications based on service selectors in a given namespace
|
||||||
func (kcl *KubeClient) getOwningApplication(namespace string, selector map[string]string) ([]models.K8sApplication, error) {
|
// for all services, it lists pods based on the service selector and converts the pod to an application
|
||||||
if len(selector) == 0 {
|
// if replicasets are found, it updates the owner reference to deployment
|
||||||
return nil, nil
|
// it then combines the service with the application
|
||||||
}
|
// finally, it returns a list of K8sServiceInfo objects
|
||||||
|
func (kcl *KubeClient) CombineServicesWithApplications(services []models.K8sServiceInfo) ([]models.K8sServiceInfo, error) {
|
||||||
selectorLabels := labels.SelectorFromSet(selector).String()
|
if containsServiceWithSelector(services) {
|
||||||
|
updatedServices := make([]models.K8sServiceInfo, len(services))
|
||||||
// look for replicasets first, limit 1 (we only support one owner)
|
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||||
replicasets, err := kcl.cli.AppsV1().ReplicaSets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selectorLabels, Limit: 1})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var meta metav1.Object
|
for index, service := range services {
|
||||||
if replicasets != nil && len(replicasets.Items) > 0 {
|
updatedService := service
|
||||||
meta = replicasets.Items[0].GetObjectMeta()
|
|
||||||
} else {
|
application, err := kcl.GetApplicationFromServiceSelector(pods, service, replicaSets)
|
||||||
// otherwise look for matching pods, limit 1 (we only support one owner)
|
|
||||||
pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selectorLabels, Limit: 1})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return services, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to get application from service. Error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pods == nil || len(pods.Items) == 0 {
|
if application != nil {
|
||||||
return nil, nil
|
updatedService.Applications = append(updatedService.Applications, *application)
|
||||||
}
|
}
|
||||||
|
|
||||||
meta = pods.Items[0].GetObjectMeta()
|
updatedServices[index] = updatedService
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeApplication(meta), nil
|
return updatedServices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return services, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeApplication(meta metav1.Object) []models.K8sApplication {
|
// containsServiceWithSelector checks if a list of services contains a service with a selector
|
||||||
ownerReferences := meta.GetOwnerReferences()
|
// it returns true if any service has a selector, otherwise false
|
||||||
if len(ownerReferences) == 0 {
|
func containsServiceWithSelector(services []models.K8sServiceInfo) bool {
|
||||||
return nil
|
for _, service := range services {
|
||||||
|
if len(service.Selector) > 0 {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently, we only support one owner reference
|
|
||||||
ownerReference := ownerReferences[0]
|
|
||||||
return []models.K8sApplication{
|
|
||||||
{
|
|
||||||
// Only the name is used right now, but we can add more fields in the future
|
|
||||||
Name: ownerReference.Name,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildServicesMap builds a map of service names from a list of K8sServiceInfo objects
|
||||||
|
// it returns a map of service names for lookups
|
||||||
|
func (kcl *KubeClient) buildServicesMap(services []models.K8sServiceInfo) map[string]struct{} {
|
||||||
|
serviceMap := make(map[string]struct{})
|
||||||
|
for _, service := range services {
|
||||||
|
serviceMap[service.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
return serviceMap
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,76 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
v1 "k8s.io/api/core/v1"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetServiceAccount returns the portainer ServiceAccountName associated to the specified user.
|
// GetServiceAccounts gets all the service accounts for either at the cluster level or a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) GetServiceAccount(tokenData *portainer.TokenData) (*v1.ServiceAccount, error) {
|
// It returns a list of K8sServiceAccount objects.
|
||||||
var portainerServiceAccountName string
|
func (kcl *KubeClient) GetServiceAccounts(namespace string) ([]models.K8sServiceAccount, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchServiceAccounts(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kcl.fetchServiceAccountsForNonAdmin(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchServiceAccountsForNonAdmin gets all the service accounts for either at the cluster level or a given namespace in a k8s endpoint.
|
||||||
|
// the namespace will be coming from NonAdminNamespaces as non-admin users are restricted to certain namespaces.
|
||||||
|
// it returns a list of K8sServiceAccount objects.
|
||||||
|
func (kcl *KubeClient) fetchServiceAccountsForNonAdmin(namespace string) ([]models.K8sServiceAccount, error) {
|
||||||
|
serviceAccounts, err := kcl.fetchServiceAccounts(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := make([]models.K8sServiceAccount, 0)
|
||||||
|
for _, serviceAccount := range serviceAccounts {
|
||||||
|
if _, ok := nonAdminNamespaceSet[serviceAccount.Namespace]; ok {
|
||||||
|
results = append(results, serviceAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchServiceAccounts returns a list of all ServiceAccounts in the specified namespace.
|
||||||
|
func (kcl *KubeClient) fetchServiceAccounts(namespace string) ([]models.K8sServiceAccount, error) {
|
||||||
|
serviceAccounts, err := kcl.cli.CoreV1().ServiceAccounts(namespace).List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]models.K8sServiceAccount, 0)
|
||||||
|
for _, serviceAccount := range serviceAccounts.Items {
|
||||||
|
results = append(results, parseServiceAccount(serviceAccount))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseServiceAccount converts a corev1.ServiceAccount object to a models.K8sServiceAccount object.
|
||||||
|
func parseServiceAccount(serviceAccount corev1.ServiceAccount) models.K8sServiceAccount {
|
||||||
|
return models.K8sServiceAccount{
|
||||||
|
Name: serviceAccount.Name,
|
||||||
|
Namespace: serviceAccount.Namespace,
|
||||||
|
CreationDate: serviceAccount.CreationTimestamp.Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPortainerUserServiceAccount returns the portainer ServiceAccountName associated to the specified user.
|
||||||
|
func (kcl *KubeClient) GetPortainerUserServiceAccount(tokenData *portainer.TokenData) (*corev1.ServiceAccount, error) {
|
||||||
|
portainerUserServiceAccountName := UserServiceAccountName(int(tokenData.ID), kcl.instanceID)
|
||||||
if tokenData.Role == portainer.AdministratorRole {
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
portainerServiceAccountName = portainerClusterAdminServiceAccountName
|
portainerUserServiceAccountName = portainerClusterAdminServiceAccountName
|
||||||
} else {
|
|
||||||
portainerServiceAccountName = UserServiceAccountName(int(tokenData.ID), kcl.instanceID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify name exists as service account resource within portainer namespace
|
// verify name exists as service account resource within portainer namespace
|
||||||
serviceAccount, err := kcl.cli.CoreV1().ServiceAccounts(portainerNamespace).Get(context.TODO(), portainerServiceAccountName, metav1.GetOptions{})
|
serviceAccount, err := kcl.cli.CoreV1().ServiceAccounts(portainerNamespace).Get(context.TODO(), portainerUserServiceAccountName, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -69,7 +122,7 @@ func (kcl *KubeClient) ensureRequiredResourcesExist() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error {
|
func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error {
|
||||||
serviceAccount := &v1.ServiceAccount{
|
serviceAccount := &corev1.ServiceAccount{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: serviceAccountName,
|
Name: serviceAccountName,
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,9 +18,9 @@ func Test_GetServiceAccount(t *testing.T) {
|
||||||
instanceID: "test",
|
instanceID: "test",
|
||||||
}
|
}
|
||||||
tokenData := &portainer.TokenData{ID: 1}
|
tokenData := &portainer.TokenData{ID: 1}
|
||||||
_, err := k.GetServiceAccount(tokenData)
|
_, err := k.GetPortainerUserServiceAccount(tokenData)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("GetServiceAccount should fail with service account not found")
|
t.Error("GetPortainerUserServiceAccount should fail with service account not found")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -46,9 +46,9 @@ func Test_GetServiceAccount(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{})
|
defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{})
|
||||||
|
|
||||||
sa, err := k.GetServiceAccount(tokenData)
|
sa, err := k.GetPortainerUserServiceAccount(tokenData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("GetServiceAccount should succeed; err=%s", err)
|
t.Errorf("GetPortainerUserServiceAccount should succeed; err=%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := "portainer-sa-clusteradmin"
|
want := "portainer-sa-clusteradmin"
|
||||||
|
@ -79,14 +79,14 @@ func Test_GetServiceAccount(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{})
|
defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{})
|
||||||
|
|
||||||
sa, err := k.GetServiceAccount(tokenData)
|
sa, err := k.GetPortainerUserServiceAccount(tokenData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("GetServiceAccount should succeed; err=%s", err)
|
t.Errorf("GetPortainerUserServiceAccount should succeed; err=%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := "portainer-sa-user-test-1"
|
want := "portainer-sa-user-test-1"
|
||||||
if sa.Name != want {
|
if sa.Name != want {
|
||||||
t.Errorf("GetServiceAccount should succeed and return correct sa name; got=%s want=%s", sa.Name, want)
|
t.Errorf("GetPortainerUserServiceAccount should succeed and return correct sa name; got=%s want=%s", sa.Name, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
258
api/kubernetes/cli/volumes.go
Normal file
258
api/kubernetes/cli/volumes.go
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
storagev1 "k8s.io/api/storage/v1"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetVolumes gets the volumes in the current k8s environment(endpoint).
|
||||||
|
// If the user is an admin, it fetches all the volumes in the cluster.
|
||||||
|
// If the user is not an admin, it fetches the volumes in the namespaces the user has access to.
|
||||||
|
// It returns a list of K8sVolumeInfo.
|
||||||
|
func (kcl *KubeClient) GetVolumes(namespace string) ([]models.K8sVolumeInfo, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchVolumes(namespace)
|
||||||
|
}
|
||||||
|
return kcl.fetchVolumesForNonAdmin(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVolume gets the volume with the given name and namespace.
|
||||||
|
func (kcl *KubeClient) GetVolume(namespace, volumeName string) (*models.K8sVolumeInfo, error) {
|
||||||
|
persistentVolumeClaim, err := kcl.cli.CoreV1().PersistentVolumeClaims(namespace).Get(context.TODO(), volumeName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
persistentVolumesMap, storageClassesMap, err := kcl.fetchPersistentVolumesAndStorageClassesMap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
volume := parseVolume(persistentVolumeClaim, persistentVolumesMap, storageClassesMap)
|
||||||
|
return &volume, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchVolumesForNonAdmin fetches the volumes in the namespaces the user has access to.
|
||||||
|
// This function is called when the user is not an admin.
|
||||||
|
// It fetches all the persistent volume claims, persistent volumes and storage classes in the namespaces the user has access to.
|
||||||
|
func (kcl *KubeClient) fetchVolumesForNonAdmin(namespace string) ([]models.K8sVolumeInfo, error) {
|
||||||
|
log.Debug().Msgf("Fetching volumes for non-admin user: %v", kcl.NonAdminNamespaces)
|
||||||
|
|
||||||
|
if len(kcl.NonAdminNamespaces) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, err := kcl.fetchVolumes(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := make([]models.K8sVolumeInfo, 0)
|
||||||
|
for _, volume := range volumes {
|
||||||
|
if _, ok := nonAdminNamespaceSet[volume.PersistentVolumeClaim.Namespace]; ok {
|
||||||
|
results = append(results, volume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchVolumes fetches all the persistent volume claims, persistent volumes and storage classes in the given namespace.
|
||||||
|
// It returns a list of K8sVolumeInfo.
|
||||||
|
// This function is called by fetchVolumesForAdmin and fetchVolumesForNonAdmin.
|
||||||
|
func (kcl *KubeClient) fetchVolumes(namespace string) ([]models.K8sVolumeInfo, error) {
|
||||||
|
volumes := make([]models.K8sVolumeInfo, 0)
|
||||||
|
persistentVolumeClaims, err := kcl.cli.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(persistentVolumeClaims.Items) > 0 {
|
||||||
|
persistentVolumesMap, storageClassesMap, err := kcl.fetchPersistentVolumesAndStorageClassesMap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, persistentVolumeClaim := range persistentVolumeClaims.Items {
|
||||||
|
volumes = append(volumes, parseVolume(&persistentVolumeClaim, persistentVolumesMap, storageClassesMap))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return volumes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVolume parses the given persistent volume claim and returns a K8sVolumeInfo.
|
||||||
|
// This function is called by fetchVolumes.
|
||||||
|
// It returns a K8sVolumeInfo.
|
||||||
|
func parseVolume(persistentVolumeClaim *corev1.PersistentVolumeClaim, persistentVolumesMap map[string]models.K8sPersistentVolume, storageClassesMap map[string]models.K8sStorageClass) models.K8sVolumeInfo {
|
||||||
|
volume := models.K8sVolumeInfo{}
|
||||||
|
volumeClaim := parsePersistentVolumeClaim(persistentVolumeClaim)
|
||||||
|
|
||||||
|
if volumeClaim.VolumeName != "" {
|
||||||
|
persistentVolume, ok := persistentVolumesMap[volumeClaim.VolumeName]
|
||||||
|
if ok {
|
||||||
|
volume.PersistentVolume = persistentVolume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if volumeClaim.StorageClass != nil {
|
||||||
|
storageClass, ok := storageClassesMap[*volumeClaim.StorageClass]
|
||||||
|
if ok {
|
||||||
|
volume.StorageClass = storageClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume.PersistentVolumeClaim = volumeClaim
|
||||||
|
return volume
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePersistentVolumeClaim parses the given persistent volume claim and returns a K8sPersistentVolumeClaim.
|
||||||
|
func parsePersistentVolumeClaim(volume *corev1.PersistentVolumeClaim) models.K8sPersistentVolumeClaim {
|
||||||
|
storage := volume.Spec.Resources.Requests[corev1.ResourceStorage]
|
||||||
|
return models.K8sPersistentVolumeClaim{
|
||||||
|
ID: string(volume.UID),
|
||||||
|
Name: volume.Name,
|
||||||
|
Namespace: volume.Namespace,
|
||||||
|
CreationDate: volume.CreationTimestamp.Time,
|
||||||
|
Storage: storage.Value(),
|
||||||
|
AccessModes: volume.Spec.AccessModes,
|
||||||
|
VolumeName: volume.Spec.VolumeName,
|
||||||
|
ResourcesRequests: &volume.Spec.Resources.Requests,
|
||||||
|
StorageClass: volume.Spec.StorageClassName,
|
||||||
|
VolumeMode: volume.Spec.VolumeMode,
|
||||||
|
OwningApplications: nil,
|
||||||
|
Phase: volume.Status.Phase,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePersistentVolume parses the given persistent volume and returns a K8sPersistentVolume.
|
||||||
|
func parsePersistentVolume(volume *corev1.PersistentVolume) models.K8sPersistentVolume {
|
||||||
|
return models.K8sPersistentVolume{
|
||||||
|
Name: volume.Name,
|
||||||
|
Annotations: volume.Annotations,
|
||||||
|
AccessModes: volume.Spec.AccessModes,
|
||||||
|
Capacity: volume.Spec.Capacity,
|
||||||
|
ClaimRef: volume.Spec.ClaimRef,
|
||||||
|
StorageClassName: volume.Spec.StorageClassName,
|
||||||
|
PersistentVolumeReclaimPolicy: volume.Spec.PersistentVolumeReclaimPolicy,
|
||||||
|
VolumeMode: volume.Spec.VolumeMode,
|
||||||
|
CSI: volume.Spec.CSI,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPersistentVolumesMap builds a map of persistent volumes.
|
||||||
|
func (kcl *KubeClient) buildPersistentVolumesMap(persistentVolumes *corev1.PersistentVolumeList) map[string]models.K8sPersistentVolume {
|
||||||
|
persistentVolumesMap := make(map[string]models.K8sPersistentVolume)
|
||||||
|
for _, persistentVolume := range persistentVolumes.Items {
|
||||||
|
persistentVolumesMap[persistentVolume.Name] = parsePersistentVolume(&persistentVolume)
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistentVolumesMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseStorageClass parses the given storage class and returns a K8sStorageClass.
|
||||||
|
func parseStorageClass(storageClass *storagev1.StorageClass) models.K8sStorageClass {
|
||||||
|
return models.K8sStorageClass{
|
||||||
|
Name: storageClass.Name,
|
||||||
|
Provisioner: storageClass.Provisioner,
|
||||||
|
ReclaimPolicy: storageClass.ReclaimPolicy,
|
||||||
|
AllowVolumeExpansion: storageClass.AllowVolumeExpansion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildStorageClassesMap builds a map of storage classes.
|
||||||
|
func (kcl *KubeClient) buildStorageClassesMap(storageClasses *storagev1.StorageClassList) map[string]models.K8sStorageClass {
|
||||||
|
storageClassesMap := make(map[string]models.K8sStorageClass)
|
||||||
|
for _, storageClass := range storageClasses.Items {
|
||||||
|
storageClassesMap[storageClass.Name] = parseStorageClass(&storageClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storageClassesMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchPersistentVolumesAndStorageClassesMap fetches all the persistent volumes and storage classes in the cluster.
|
||||||
|
// It returns a map of persistent volumes and a map of storage classes.
|
||||||
|
func (kcl *KubeClient) fetchPersistentVolumesAndStorageClassesMap() (map[string]models.K8sPersistentVolume, map[string]models.K8sStorageClass, error) {
|
||||||
|
persistentVolumes, err := kcl.cli.CoreV1().PersistentVolumes().List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
persistentVolumesMap := kcl.buildPersistentVolumesMap(persistentVolumes)
|
||||||
|
|
||||||
|
storageClasses, err := kcl.cli.StorageV1().StorageClasses().List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
storageClassesMap := kcl.buildStorageClassesMap(storageClasses)
|
||||||
|
|
||||||
|
return persistentVolumesMap, storageClassesMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombineVolumesWithApplications combines the volumes with the applications that use them.
|
||||||
|
func (kcl *KubeClient) CombineVolumesWithApplications(volumes *[]models.K8sVolumeInfo) (*[]models.K8sVolumeInfo, error) {
|
||||||
|
pods, err := kcl.cli.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsNotFound(err) {
|
||||||
|
return volumes, nil
|
||||||
|
}
|
||||||
|
log.Error().Err(err).Msg("Failed to list pods across the cluster")
|
||||||
|
return nil, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to list pods across the cluster. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasReplicaSetOwnerReference := containsReplicaSetOwnerReference(pods)
|
||||||
|
replicaSetItems := make([]appsv1.ReplicaSet, 0)
|
||||||
|
if hasReplicaSetOwnerReference {
|
||||||
|
replicaSets, err := kcl.cli.AppsV1().ReplicaSets("").List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to list replica sets across the cluster")
|
||||||
|
return nil, fmt.Errorf("an error occurred during the CombineVolumesWithApplications operation, unable to list replica sets across the cluster. Error: %w", err)
|
||||||
|
}
|
||||||
|
replicaSetItems = replicaSets.Items
|
||||||
|
}
|
||||||
|
|
||||||
|
return kcl.updateVolumesWithOwningApplications(volumes, pods, replicaSetItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateVolumesWithOwningApplications updates the volumes with the applications that use them.
|
||||||
|
func (kcl *KubeClient) updateVolumesWithOwningApplications(volumes *[]models.K8sVolumeInfo, pods *corev1.PodList, replicaSetItems []appsv1.ReplicaSet) (*[]models.K8sVolumeInfo, error) {
|
||||||
|
for i, volume := range *volumes {
|
||||||
|
for _, pod := range pods.Items {
|
||||||
|
if pod.Spec.Volumes != nil {
|
||||||
|
for _, podVolume := range pod.Spec.Volumes {
|
||||||
|
if podVolume.PersistentVolumeClaim != nil && podVolume.PersistentVolumeClaim.ClaimName == volume.PersistentVolumeClaim.Name && pod.Namespace == volume.PersistentVolumeClaim.Namespace {
|
||||||
|
application, err := kcl.ConvertPodToApplication(pod, replicaSetItems, []appsv1.Deployment{}, []appsv1.StatefulSet{}, []appsv1.DaemonSet{}, []corev1.Service{}, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to convert pod to application")
|
||||||
|
return nil, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to convert pod to application. Error: %w", err)
|
||||||
|
}
|
||||||
|
// Check if the application already exists in the OwningApplications slice
|
||||||
|
exists := false
|
||||||
|
for _, existingApp := range (*volumes)[i].PersistentVolumeClaim.OwningApplications {
|
||||||
|
if existingApp.Name == application.Name && existingApp.Namespace == application.Namespace {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists && application != nil {
|
||||||
|
(*volumes)[i].PersistentVolumeClaim.OwningApplications = append((*volumes)[i].PersistentVolumeClaim.OwningApplications, *application)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return volumes, nil
|
||||||
|
}
|
|
@ -57,7 +57,7 @@ func (h *HandlerDeleteK8sRegistrySecrets) Execute(pa portainer.PendingAction, en
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
kubeClient, err := h.kubeFactory.GetKubeClient(endpoint)
|
kubeClient, err := h.kubeFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ func (service *PendingActionsService) execute(environmentID portainer.EndpointID
|
||||||
} else {
|
} else {
|
||||||
// For Kubernetes endpoints, we need to check if the endpoint is up by
|
// For Kubernetes endpoints, we need to check if the endpoint is up by
|
||||||
// creating a kube client and performing a simple operation
|
// creating a kube client and performing a simple operation
|
||||||
client, err := service.kubeFactory.GetKubeClient(endpoint)
|
client, err := service.kubeFactory.GetPrivilegedKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Msgf("failed to create Kubernetes client for environment %d: %v", environmentID, err)
|
log.Debug().Msgf("failed to create Kubernetes client for environment %d: %v", environmentID, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/portainer/portainer/pkg/featureflags"
|
"github.com/portainer/portainer/pkg/featureflags"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
v1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/version"
|
"k8s.io/apimachinery/pkg/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -595,8 +595,14 @@ type (
|
||||||
JobType int
|
JobType int
|
||||||
|
|
||||||
K8sNamespaceInfo struct {
|
K8sNamespaceInfo struct {
|
||||||
|
Id string `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Status corev1.NamespaceStatus `json:"Status"`
|
||||||
|
CreationDate string `json:"CreationDate"`
|
||||||
|
NamespaceOwner string `json:"NamespaceOwner"`
|
||||||
IsSystem bool `json:"IsSystem"`
|
IsSystem bool `json:"IsSystem"`
|
||||||
IsDefault bool `json:"IsDefault"`
|
IsDefault bool `json:"IsDefault"`
|
||||||
|
ResourceQuota *corev1.ResourceQuota `json:"ResourceQuota"`
|
||||||
}
|
}
|
||||||
|
|
||||||
K8sNodeLimits struct {
|
K8sNodeLimits struct {
|
||||||
|
@ -1491,22 +1497,22 @@ type (
|
||||||
|
|
||||||
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
||||||
IsRBACEnabled() (bool, error)
|
IsRBACEnabled() (bool, error)
|
||||||
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
|
GetPortainerUserServiceAccount(tokendata *TokenData) (*corev1.ServiceAccount, error)
|
||||||
GetServiceAccountBearerToken(userID int) (string, error)
|
GetServiceAccountBearerToken(userID int) (string, error)
|
||||||
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
|
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
|
||||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
|
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
|
||||||
|
|
||||||
HasStackName(namespace string, stackName string) (bool, error)
|
HasStackName(namespace string, stackName string) (bool, error)
|
||||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||||
CreateNamespace(info models.K8sNamespaceDetails) error
|
CreateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error)
|
||||||
UpdateNamespace(info models.K8sNamespaceDetails) error
|
UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error)
|
||||||
GetNamespaces() (map[string]K8sNamespaceInfo, error)
|
GetNamespaces() (map[string]K8sNamespaceInfo, error)
|
||||||
GetNamespace(string) (K8sNamespaceInfo, error)
|
GetNamespace(string) (K8sNamespaceInfo, error)
|
||||||
DeleteNamespace(namespace string) error
|
DeleteNamespace(namespace string) (*corev1.Namespace, error)
|
||||||
GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error)
|
GetConfigMaps(namespace string) ([]models.K8sConfigMap, error)
|
||||||
|
GetSecrets(namespace string) ([]models.K8sSecret, error)
|
||||||
GetIngressControllers() (models.K8sIngressControllers, error)
|
GetIngressControllers() (models.K8sIngressControllers, error)
|
||||||
GetApplications(namespace, kind string) ([]models.K8sApplication, error)
|
GetApplications(namespace, nodename string, withDependencies bool) ([]models.K8sApplication, error)
|
||||||
GetApplication(namespace, kind, name string) (models.K8sApplication, error)
|
|
||||||
GetMetrics() (models.K8sMetrics, error)
|
GetMetrics() (models.K8sMetrics, error)
|
||||||
GetStorage() ([]KubernetesStorageClassConfig, error)
|
GetStorage() ([]KubernetesStorageClassConfig, error)
|
||||||
CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error
|
CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error
|
||||||
|
@ -1515,7 +1521,7 @@ type (
|
||||||
DeleteIngresses(reqs models.K8sIngressDeleteRequests) error
|
DeleteIngresses(reqs models.K8sIngressDeleteRequests) error
|
||||||
CreateService(namespace string, service models.K8sServiceInfo) error
|
CreateService(namespace string, service models.K8sServiceInfo) error
|
||||||
UpdateService(namespace string, service models.K8sServiceInfo) error
|
UpdateService(namespace string, service models.K8sServiceInfo) error
|
||||||
GetServices(namespace string, lookupApplications bool) ([]models.K8sServiceInfo, error)
|
GetServices(namespace string) ([]models.K8sServiceInfo, error)
|
||||||
DeleteServices(reqs models.K8sServiceDeleteRequests) error
|
DeleteServices(reqs models.K8sServiceDeleteRequests) error
|
||||||
GetNodesLimits() (K8sNodesLimits, error)
|
GetNodesLimits() (K8sNodesLimits, error)
|
||||||
GetMaxResourceLimits(name string, overCommitEnabled bool, resourceOverCommitPercent int) (K8sNodeLimits, error)
|
GetMaxResourceLimits(name string, overCommitEnabled bool, resourceOverCommitPercent int) (K8sNodeLimits, error)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { EnvironmentStatus } from '@/react/portainer/environments/types';
|
import { EnvironmentStatus } from '@/react/portainer/environments/types';
|
||||||
import { getSelfSubjectAccessReview } from '@/react/kubernetes/namespaces/getSelfSubjectAccessReview';
|
|
||||||
|
|
||||||
import { updateAxiosAdapter } from '@/portainer/services/axios';
|
import { updateAxiosAdapter } from '@/portainer/services/axios';
|
||||||
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||||
|
@ -96,13 +95,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const status = await checkEndpointStatus(
|
const status = await checkEndpointStatus(endpoint);
|
||||||
endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
|
|
||||||
? KubernetesHealthService.ping(endpoint.Id)
|
|
||||||
: // use selfsubject access review to check if we can connect to the kubernetes environment
|
|
||||||
// because it gets a fast response, and is accessible to all users
|
|
||||||
getSelfSubjectAccessReview(endpoint.Id, 'default')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
if (endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
||||||
await updateEndpointStatus(endpoint, status);
|
await updateEndpointStatus(endpoint, status);
|
||||||
|
@ -131,9 +124,9 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkEndpointStatus(promise) {
|
async function checkEndpointStatus(endpoint) {
|
||||||
try {
|
try {
|
||||||
await promise;
|
await KubernetesHealthService.ping(endpoint.Id);
|
||||||
return EnvironmentStatus.Up;
|
return EnvironmentStatus.Up;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return EnvironmentStatus.Down;
|
return EnvironmentStatus.Down;
|
||||||
|
@ -459,10 +452,10 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
|
|
||||||
const resourcePools = {
|
const resourcePools = {
|
||||||
name: 'kubernetes.resourcePools',
|
name: 'kubernetes.resourcePools',
|
||||||
url: '/pools',
|
url: '/namespaces',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'kubernetesResourcePoolsView',
|
component: 'kubernetesNamespacesView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
@ -511,7 +504,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
|
|
||||||
const volumes = {
|
const volumes = {
|
||||||
name: 'kubernetes.volumes',
|
name: 'kubernetes.volumes',
|
||||||
url: '/volumes',
|
url: '/volumes?tab',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'kubernetesVolumesView',
|
component: 'kubernetesVolumesView',
|
||||||
|
@ -582,6 +575,51 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const moreResources = {
|
||||||
|
name: 'kubernetes.moreResources',
|
||||||
|
url: '/moreResources',
|
||||||
|
abstract: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceAccounts = {
|
||||||
|
name: 'kubernetes.moreResources.serviceAccounts',
|
||||||
|
url: '/serviceAccounts',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
component: 'serviceAccountsView',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
docs: '/user/kubernetes/more-resources/service-accounts',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const clusterRoles = {
|
||||||
|
name: 'kubernetes.moreResources.clusterRoles',
|
||||||
|
url: '/clusterRoles?tab',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
component: 'clusterRolesView',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
docs: '/user/kubernetes/more-resources/cluster-roles',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const roles = {
|
||||||
|
name: 'kubernetes.moreResources.roles',
|
||||||
|
url: '/roles?tab',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
component: 'k8sRolesView',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
docs: '/user/kubernetes/more-resources/namespace-roles',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
$stateRegistryProvider.register(kubernetes);
|
$stateRegistryProvider.register(kubernetes);
|
||||||
$stateRegistryProvider.register(helmApplication);
|
$stateRegistryProvider.register(helmApplication);
|
||||||
$stateRegistryProvider.register(applications);
|
$stateRegistryProvider.register(applications);
|
||||||
|
@ -621,5 +659,10 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
$stateRegistryProvider.register(ingresses);
|
$stateRegistryProvider.register(ingresses);
|
||||||
$stateRegistryProvider.register(ingressesCreate);
|
$stateRegistryProvider.register(ingressesCreate);
|
||||||
$stateRegistryProvider.register(ingressesEdit);
|
$stateRegistryProvider.register(ingressesEdit);
|
||||||
|
|
||||||
|
$stateRegistryProvider.register(moreResources);
|
||||||
|
$stateRegistryProvider.register(serviceAccounts);
|
||||||
|
$stateRegistryProvider.register(clusterRoles);
|
||||||
|
$stateRegistryProvider.register(roles);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -77,7 +77,7 @@ class KubernetesConfigMapConverter {
|
||||||
static createPayload(data) {
|
static createPayload(data) {
|
||||||
const res = new KubernetesConfigMapCreatePayload();
|
const res = new KubernetesConfigMapCreatePayload();
|
||||||
res.metadata.name = data.Name;
|
res.metadata.name = data.Name;
|
||||||
res.metadata.namespace = data.Namespace;
|
res.metadata.namespace = data.Namespace.Namespace.Name;
|
||||||
const configurationOwner = _.truncate(data.ConfigurationOwner, { length: 63, omission: '' });
|
const configurationOwner = _.truncate(data.ConfigurationOwner, { length: 63, omission: '' });
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ class KubernetesConfigMapConverter {
|
||||||
const res = new KubernetesConfigMap();
|
const res = new KubernetesConfigMap();
|
||||||
res.Id = formValues.Id;
|
res.Id = formValues.Id;
|
||||||
res.Name = formValues.Name;
|
res.Name = formValues.Name;
|
||||||
res.Namespace = formValues.ResourcePool.Namespace.Name;
|
res.Namespace = formValues.ResourcePool;
|
||||||
res.ConfigurationOwner = formValues.ConfigurationOwner;
|
res.ConfigurationOwner = formValues.ConfigurationOwner;
|
||||||
res.Data = formValues.Data;
|
res.Data = formValues.Data;
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -9,7 +9,7 @@ class KubernetesSecretConverter {
|
||||||
static createPayload(secret) {
|
static createPayload(secret) {
|
||||||
const res = new KubernetesSecretCreatePayload();
|
const res = new KubernetesSecretCreatePayload();
|
||||||
res.metadata.name = secret.Name;
|
res.metadata.name = secret.Name;
|
||||||
res.metadata.namespace = secret.Namespace;
|
res.metadata.namespace = secret.Namespace.Namespace.Name;
|
||||||
res.type = secret.Type;
|
res.type = secret.Type;
|
||||||
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
|
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
||||||
|
@ -100,7 +100,7 @@ class KubernetesSecretConverter {
|
||||||
static configurationFormValuesToSecret(formValues) {
|
static configurationFormValuesToSecret(formValues) {
|
||||||
const res = new KubernetesApplicationSecret();
|
const res = new KubernetesApplicationSecret();
|
||||||
res.Name = formValues.Name;
|
res.Name = formValues.Name;
|
||||||
res.Namespace = formValues.ResourcePool.Namespace.Name;
|
res.Namespace = formValues.ResourcePool;
|
||||||
res.Type = formValues.Type;
|
res.Type = formValues.Type;
|
||||||
res.ConfigurationOwner = formValues.ConfigurationOwner;
|
res.ConfigurationOwner = formValues.ConfigurationOwner;
|
||||||
res.Data = formValues.Data;
|
res.Data = formValues.Data;
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import _ from 'lodash-es';
|
|
||||||
import { KubernetesStack } from 'Kubernetes/models/stack/models';
|
|
||||||
|
|
||||||
class KubernetesStackHelper {
|
|
||||||
static stacksFromApplications(applications) {
|
|
||||||
const res = _.reduce(
|
|
||||||
applications,
|
|
||||||
(acc, app) => {
|
|
||||||
if (app.StackName) {
|
|
||||||
let stack = _.find(acc, { Name: app.StackName, ResourcePool: app.ResourcePool });
|
|
||||||
if (!stack) {
|
|
||||||
stack = new KubernetesStack();
|
|
||||||
stack.Name = app.StackName;
|
|
||||||
stack.ResourcePool = app.ResourcePool;
|
|
||||||
acc.push(stack);
|
|
||||||
}
|
|
||||||
stack.Applications.push(app);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default KubernetesStackHelper;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import KubernetesStackHelper from './stackHelper';
|
|
||||||
|
|
||||||
describe('stacksFromApplications', () => {
|
|
||||||
const { stacksFromApplications } = KubernetesStackHelper;
|
|
||||||
test('should return an empty array when passed an empty array', () => {
|
|
||||||
expect(stacksFromApplications([])).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return an empty array when passed a list of applications without stacks', () => {
|
|
||||||
expect(stacksFromApplications([{ StackName: '' }, { StackName: '' }, { StackName: '' }, { StackName: '' }])).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -41,9 +41,11 @@ export class Application {
|
||||||
annotations?: Record<string, string>;
|
annotations?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
Limits: {
|
Resource?: {
|
||||||
Cpu?: number;
|
cpuLimit?: number;
|
||||||
Memory?: number;
|
cpuRequest?: number;
|
||||||
|
memoryLimit?: number;
|
||||||
|
memoryRequest?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
ServiceType?: ServiceType;
|
ServiceType?: ServiceType;
|
||||||
|
@ -106,7 +108,7 @@ export class Application {
|
||||||
this.Pods = [];
|
this.Pods = [];
|
||||||
this.Containers = [];
|
this.Containers = [];
|
||||||
this.Metadata = {};
|
this.Metadata = {};
|
||||||
this.Limits = {};
|
this.Resource = {};
|
||||||
this.ServiceId = '';
|
this.ServiceId = '';
|
||||||
this.ServiceName = '';
|
this.ServiceName = '';
|
||||||
this.HeadlessServiceName = undefined;
|
this.HeadlessServiceName = undefined;
|
||||||
|
|
|
@ -3,5 +3,7 @@ export class StorageClass {
|
||||||
|
|
||||||
Provisioner: string = '';
|
Provisioner: string = '';
|
||||||
|
|
||||||
|
ReclaimPolicy: string = '';
|
||||||
|
|
||||||
AllowVolumeExpansion: boolean = false;
|
AllowVolumeExpansion: boolean = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { PersistentVolumeClaim } from './PersistentVolumeClaim';
|
||||||
type VolumeResourcePool = ReturnType<typeof KubernetesResourcePool>;
|
type VolumeResourcePool = ReturnType<typeof KubernetesResourcePool>;
|
||||||
|
|
||||||
export class Volume {
|
export class Volume {
|
||||||
ResourcePool: VolumeResourcePool = {} as VolumeResourcePool;
|
ResourcePool?: VolumeResourcePool = {} as VolumeResourcePool;
|
||||||
|
|
||||||
PersistentVolumeClaim: PersistentVolumeClaim = {} as PersistentVolumeClaim;
|
PersistentVolumeClaim: PersistentVolumeClaim = {} as PersistentVolumeClaim;
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,6 @@ export const applicationsModule = angular
|
||||||
.component(
|
.component(
|
||||||
'kubernetesApplicationsDatatable',
|
'kubernetesApplicationsDatatable',
|
||||||
r2a(withUIRouter(withCurrentUser(ApplicationsDatatable)), [
|
r2a(withUIRouter(withCurrentUser(ApplicationsDatatable)), [
|
||||||
'dataset',
|
|
||||||
'isLoading',
|
|
||||||
'namespace',
|
'namespace',
|
||||||
'namespaces',
|
'namespaces',
|
||||||
'onNamespaceChange',
|
'onNamespaceChange',
|
||||||
|
|
|
@ -11,11 +11,7 @@ export const clusterManagementModule = angular
|
||||||
.module('portainer.kubernetes.react.components.clusterManagement', [])
|
.module('portainer.kubernetes.react.components.clusterManagement', [])
|
||||||
.component(
|
.component(
|
||||||
'kubernetesNodeApplicationsDatatable',
|
'kubernetesNodeApplicationsDatatable',
|
||||||
r2a(withUIRouter(withCurrentUser(NodeApplicationsDatatable)), [
|
r2a(withUIRouter(withCurrentUser(NodeApplicationsDatatable)), [])
|
||||||
'dataset',
|
|
||||||
'isLoading',
|
|
||||||
'onRefresh',
|
|
||||||
])
|
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'resourceEventsDatatable',
|
'resourceEventsDatatable',
|
||||||
|
|
|
@ -62,7 +62,6 @@ import { kubeEnvVarValidationSchema } from '@/react/kubernetes/applications/comp
|
||||||
import { IntegratedAppsDatatable } from '@/react/kubernetes/components/IntegratedAppsDatatable/IntegratedAppsDatatable';
|
import { IntegratedAppsDatatable } from '@/react/kubernetes/components/IntegratedAppsDatatable/IntegratedAppsDatatable';
|
||||||
|
|
||||||
import { applicationsModule } from './applications';
|
import { applicationsModule } from './applications';
|
||||||
import { volumesModule } from './volumes';
|
|
||||||
import { namespacesModule } from './namespaces';
|
import { namespacesModule } from './namespaces';
|
||||||
import { clusterManagementModule } from './clusterManagement';
|
import { clusterManagementModule } from './clusterManagement';
|
||||||
import { registriesModule } from './registries';
|
import { registriesModule } from './registries';
|
||||||
|
@ -70,7 +69,6 @@ import { registriesModule } from './registries';
|
||||||
export const ngModule = angular
|
export const ngModule = angular
|
||||||
.module('portainer.kubernetes.react.components', [
|
.module('portainer.kubernetes.react.components', [
|
||||||
applicationsModule,
|
applicationsModule,
|
||||||
volumesModule,
|
|
||||||
namespacesModule,
|
namespacesModule,
|
||||||
clusterManagementModule,
|
clusterManagementModule,
|
||||||
registriesModule,
|
registriesModule,
|
||||||
|
@ -213,13 +211,10 @@ export const ngModule = angular
|
||||||
.component(
|
.component(
|
||||||
'kubernetesApplicationsStacksDatatable',
|
'kubernetesApplicationsStacksDatatable',
|
||||||
r2a(withUIRouter(withCurrentUser(ApplicationsStacksDatatable)), [
|
r2a(withUIRouter(withCurrentUser(ApplicationsStacksDatatable)), [
|
||||||
'dataset',
|
|
||||||
'onRefresh',
|
|
||||||
'onRemove',
|
'onRemove',
|
||||||
'namespace',
|
'namespace',
|
||||||
'namespaces',
|
'namespaces',
|
||||||
'onNamespaceChange',
|
'onNamespaceChange',
|
||||||
'isLoading',
|
|
||||||
'showSystem',
|
'showSystem',
|
||||||
'setSystemResources',
|
'setSystemResources',
|
||||||
])
|
])
|
||||||
|
|
|
@ -12,11 +12,7 @@ export const namespacesModule = angular
|
||||||
.module('portainer.kubernetes.react.components.namespaces', [])
|
.module('portainer.kubernetes.react.components.namespaces', [])
|
||||||
.component(
|
.component(
|
||||||
'kubernetesNamespacesDatatable',
|
'kubernetesNamespacesDatatable',
|
||||||
r2a(withUIRouter(withCurrentUser(NamespacesDatatable)), [
|
r2a(withUIRouter(withCurrentUser(NamespacesDatatable)), [])
|
||||||
'dataset',
|
|
||||||
'onRemove',
|
|
||||||
'onRefresh',
|
|
||||||
])
|
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'kubernetesNamespaceApplicationsDatatable',
|
'kubernetesNamespaceApplicationsDatatable',
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
|
||||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
|
||||||
import { VolumesDatatable } from '@/react/kubernetes/volumes/ListView/VolumesDatatable';
|
|
||||||
import { StorageDatatable } from '@/react/kubernetes/volumes/ListView/StorageDatatable';
|
|
||||||
|
|
||||||
export const volumesModule = angular
|
|
||||||
.module('portainer.kubernetes.react.components.volumes', [])
|
|
||||||
.component(
|
|
||||||
'kubernetesVolumesDatatable',
|
|
||||||
r2a(withUIRouter(withCurrentUser(VolumesDatatable)), [
|
|
||||||
'dataset',
|
|
||||||
'onRemove',
|
|
||||||
'onRefresh',
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.component(
|
|
||||||
'kubernetesVolumesStoragesDatatable',
|
|
||||||
r2a(withUIRouter(withCurrentUser(StorageDatatable)), [
|
|
||||||
'dataset',
|
|
||||||
'onRefresh',
|
|
||||||
])
|
|
||||||
).name;
|
|
|
@ -13,6 +13,11 @@ import { ConfigmapsAndSecretsView } from '@/react/kubernetes/configs/ListView/Co
|
||||||
import { CreateNamespaceView } from '@/react/kubernetes/namespaces/CreateView/CreateNamespaceView';
|
import { CreateNamespaceView } from '@/react/kubernetes/namespaces/CreateView/CreateNamespaceView';
|
||||||
import { ApplicationDetailsView } from '@/react/kubernetes/applications/DetailsView/ApplicationDetailsView';
|
import { ApplicationDetailsView } from '@/react/kubernetes/applications/DetailsView/ApplicationDetailsView';
|
||||||
import { ConfigureView } from '@/react/kubernetes/cluster/ConfigureView';
|
import { ConfigureView } from '@/react/kubernetes/cluster/ConfigureView';
|
||||||
|
import { NamespacesView } from '@/react/kubernetes/namespaces/ListView/NamespacesView';
|
||||||
|
import { ServiceAccountsView } from '@/react/kubernetes/more-resources/ServiceAccountsView/ServiceAccountsView';
|
||||||
|
import { ClusterRolesView } from '@/react/kubernetes/more-resources/ClusterRolesView';
|
||||||
|
import { RolesView } from '@/react/kubernetes/more-resources/RolesView';
|
||||||
|
import { VolumesView } from '@/react/kubernetes/volumes/ListView/VolumesView';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
.module('portainer.kubernetes.react.views', [])
|
.module('portainer.kubernetes.react.views', [])
|
||||||
|
@ -20,10 +25,18 @@ export const viewsModule = angular
|
||||||
'kubernetesCreateNamespaceView',
|
'kubernetesCreateNamespaceView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(CreateNamespaceView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(CreateNamespaceView))), [])
|
||||||
)
|
)
|
||||||
|
.component(
|
||||||
|
'kubernetesNamespacesView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(NamespacesView))), [])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'kubernetesServicesView',
|
'kubernetesServicesView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ServicesView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(ServicesView))), [])
|
||||||
)
|
)
|
||||||
|
.component(
|
||||||
|
'kubernetesVolumesView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(VolumesView))), [])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'kubernetesIngressesView',
|
'kubernetesIngressesView',
|
||||||
r2a(
|
r2a(
|
||||||
|
@ -60,4 +73,16 @@ export const viewsModule = angular
|
||||||
.component(
|
.component(
|
||||||
'kubernetesConsoleView',
|
'kubernetesConsoleView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ConsoleView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(ConsoleView))), [])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'serviceAccountsView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(ServiceAccountsView))), [])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'clusterRolesView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(ClusterRolesView))), [])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'k8sRolesView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(RolesView))), [])
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -14,7 +14,6 @@ import { notifyError } from '@/portainer/services/notifications';
|
||||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||||
import { generateNewIngressesFromFormPaths } from '@/react/kubernetes/applications/CreateView/application-services/utils';
|
import { generateNewIngressesFromFormPaths } from '@/react/kubernetes/applications/CreateView/application-services/utils';
|
||||||
import { KubernetesPod } from '../pod/models';
|
import { KubernetesPod } from '../pod/models';
|
||||||
import { KubernetesApplication } from '../models/application/models';
|
|
||||||
|
|
||||||
class KubernetesApplicationService {
|
class KubernetesApplicationService {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
@ -64,7 +63,7 @@ class KubernetesApplicationService {
|
||||||
apiService = this.KubernetesDaemonSetService;
|
apiService = this.KubernetesDaemonSetService;
|
||||||
} else if (app.ApplicationType === KubernetesApplicationTypes.StatefulSet) {
|
} else if (app.ApplicationType === KubernetesApplicationTypes.StatefulSet) {
|
||||||
apiService = this.KubernetesStatefulSetService;
|
apiService = this.KubernetesStatefulSetService;
|
||||||
} else if (app instanceof KubernetesPod || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.Pod)) {
|
} else if (app instanceof KubernetesPod || KubernetesApplicationTypes.Pod) {
|
||||||
apiService = this.KubernetesPodService;
|
apiService = this.KubernetesPodService;
|
||||||
} else {
|
} else {
|
||||||
throw new PortainerError('Unable to determine which association to use to retrieve API Service');
|
throw new PortainerError('Unable to determine which association to use to retrieve API Service');
|
||||||
|
|
|
@ -2,17 +2,17 @@ import angular from 'angular';
|
||||||
import PortainerError from 'Portainer/error';
|
import PortainerError from 'Portainer/error';
|
||||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||||
import KubernetesNamespaceConverter from 'Kubernetes/converters/namespace';
|
import KubernetesNamespaceConverter from 'Kubernetes/converters/namespace';
|
||||||
|
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||||
import { updateNamespaces } from 'Kubernetes/store/namespace';
|
import { updateNamespaces } from 'Kubernetes/store/namespace';
|
||||||
import $allSettled from 'Portainer/services/allSettled';
|
|
||||||
import { getSelfSubjectAccessReview } from '@/react/kubernetes/namespaces/getSelfSubjectAccessReview';
|
|
||||||
|
|
||||||
class KubernetesNamespaceService {
|
class KubernetesNamespaceService {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, KubernetesNamespaces, LocalStorage, $state) {
|
constructor($async, KubernetesNamespaces, Authentication, LocalStorage, $state) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.KubernetesNamespaces = KubernetesNamespaces;
|
this.KubernetesNamespaces = KubernetesNamespaces;
|
||||||
this.LocalStorage = LocalStorage;
|
this.LocalStorage = LocalStorage;
|
||||||
|
this.Authentication = Authentication;
|
||||||
|
|
||||||
this.getAsync = this.getAsync.bind(this);
|
this.getAsync = this.getAsync.bind(this);
|
||||||
this.getAllAsync = this.getAllAsync.bind(this);
|
this.getAllAsync = this.getAllAsync.bind(this);
|
||||||
|
@ -68,17 +68,13 @@ class KubernetesNamespaceService {
|
||||||
try {
|
try {
|
||||||
// get the list of all namespaces (RBAC allows users to see the list of namespaces)
|
// get the list of all namespaces (RBAC allows users to see the list of namespaces)
|
||||||
const data = await this.KubernetesNamespaces().get().$promise;
|
const data = await this.KubernetesNamespaces().get().$promise;
|
||||||
// get the status of each namespace with accessReviews (to avoid failed forbidden responses, which aren't cached)
|
// get the list of all namespaces with isAccessAllowed flags
|
||||||
const accessReviews = await Promise.all(data.items.map((namespace) => getSelfSubjectAccessReview(this.$state.params.endpointId, namespace.metadata.name)));
|
const hasK8sAccessSystemNamespaces = this.Authentication.hasAuthorizations(['K8sAccessSystemNamespaces']);
|
||||||
const allowedNamespaceNames = accessReviews.filter((ar) => ar.status.allowed).map((ar) => ar.spec.resourceAttributes.namespace);
|
const namespaces = data.items.filter((item) => !KubernetesNamespaceHelper.isSystemNamespace(item.metadata.name) || hasK8sAccessSystemNamespaces);
|
||||||
const promises = allowedNamespaceNames.map((name) => this.KubernetesNamespaces().status({ id: name }).$promise);
|
// parse the namespaces
|
||||||
const namespaces = await $allSettled(promises);
|
const visibleNamespaces = namespaces.map((item) => KubernetesNamespaceConverter.apiToNamespace(item));
|
||||||
// only return namespaces if the user has access to namespaces
|
updateNamespaces(visibleNamespaces);
|
||||||
const allNamespaces = namespaces.fulfilled.map((item) => {
|
return visibleNamespaces;
|
||||||
return KubernetesNamespaceConverter.apiToNamespace(item);
|
|
||||||
});
|
|
||||||
updateNamespaces(allNamespaces);
|
|
||||||
return allNamespaces;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new PortainerError('Unable to retrieve namespaces', err);
|
throw new PortainerError('Unable to retrieve namespaces', err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
|
||||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
|
||||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
|
||||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
|
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||||
import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
|
import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
|
||||||
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
|
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
|
||||||
|
import { getStacksFromApplications } from '@/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/getStacksFromApplications';
|
||||||
|
import { getApplications } from '@/react/kubernetes/applications/application.queries.ts';
|
||||||
|
import { getNamespaces } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||||
class KubernetesApplicationsController {
|
class KubernetesApplicationsController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -16,6 +15,7 @@ class KubernetesApplicationsController {
|
||||||
Authentication,
|
Authentication,
|
||||||
Notifications,
|
Notifications,
|
||||||
KubernetesApplicationService,
|
KubernetesApplicationService,
|
||||||
|
EndpointService,
|
||||||
HelmService,
|
HelmService,
|
||||||
KubernetesConfigurationService,
|
KubernetesConfigurationService,
|
||||||
LocalStorage,
|
LocalStorage,
|
||||||
|
@ -36,8 +36,6 @@ class KubernetesApplicationsController {
|
||||||
this.KubernetesNamespaceService = KubernetesNamespaceService;
|
this.KubernetesNamespaceService = KubernetesNamespaceService;
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
this.onInit = this.onInit.bind(this);
|
||||||
this.getApplications = this.getApplications.bind(this);
|
|
||||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
|
||||||
this.removeAction = this.removeAction.bind(this);
|
this.removeAction = this.removeAction.bind(this);
|
||||||
this.removeActionAsync = this.removeActionAsync.bind(this);
|
this.removeActionAsync = this.removeActionAsync.bind(this);
|
||||||
this.removeStacksAction = this.removeStacksAction.bind(this);
|
this.removeStacksAction = this.removeStacksAction.bind(this);
|
||||||
|
@ -88,23 +86,18 @@ class KubernetesApplicationsController {
|
||||||
if (application.ApplicationType === KubernetesApplicationTypes.Helm) {
|
if (application.ApplicationType === KubernetesApplicationTypes.Helm) {
|
||||||
await this.HelmService.uninstall(this.endpoint.Id, application);
|
await this.HelmService.uninstall(this.endpoint.Id, application);
|
||||||
} else {
|
} else {
|
||||||
await this.KubernetesApplicationService.delete(application);
|
|
||||||
|
|
||||||
if (application.Metadata.labels && application.Metadata.labels[KubernetesPortainerApplicationStackNameLabel]) {
|
if (application.Metadata.labels && application.Metadata.labels[KubernetesPortainerApplicationStackNameLabel]) {
|
||||||
// Update applications in stack
|
|
||||||
const stack = this.state.stacks.find((x) => x.Name === application.StackName);
|
|
||||||
const index = stack.Applications.indexOf(application);
|
|
||||||
stack.Applications.splice(index, 1);
|
|
||||||
|
|
||||||
// remove stack if no app left in the stack
|
// remove stack if no app left in the stack
|
||||||
|
const appsInNamespace = await getApplications(this.endpoint.Id, { namespace: application.ResourcePool, withDependencies: false });
|
||||||
|
const stacksInNamespace = getStacksFromApplications(appsInNamespace);
|
||||||
|
const stack = stacksInNamespace.find((x) => x.Name === application.StackName);
|
||||||
if (stack.Applications.length === 0 && application.StackId) {
|
if (stack.Applications.length === 0 && application.StackId) {
|
||||||
await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id);
|
await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await this.KubernetesApplicationService.delete(application);
|
||||||
}
|
}
|
||||||
this.Notifications.success('Application successfully removed', application.Name);
|
this.Notifications.success('Application successfully removed', application.Name);
|
||||||
const index = this.state.applications.indexOf(application);
|
|
||||||
this.state.applications.splice(index, 1);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to remove application');
|
this.Notifications.error('Failure', err, 'Unable to remove application');
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -137,42 +130,15 @@ class KubernetesApplicationsController {
|
||||||
this.state.namespaceName = namespaceName;
|
this.state.namespaceName = namespaceName;
|
||||||
// save the selected namespaceName in local storage with the key 'kubernetes_namespace_filter_${environmentId}_${userID}'
|
// save the selected namespaceName in local storage with the key 'kubernetes_namespace_filter_${environmentId}_${userID}'
|
||||||
this.LocalStorage.storeNamespaceFilter(this.endpoint.Id, this.user.ID, namespaceName);
|
this.LocalStorage.storeNamespaceFilter(this.endpoint.Id, this.user.ID, namespaceName);
|
||||||
return this.getApplicationsAsync();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApplicationsAsync() {
|
|
||||||
try {
|
|
||||||
this.state.isAppsLoading = true;
|
|
||||||
const [applications, configurations] = await Promise.all([
|
|
||||||
this.KubernetesApplicationService.get(this.state.namespaceName),
|
|
||||||
this.KubernetesConfigurationService.get(this.state.namespaceName),
|
|
||||||
]);
|
|
||||||
const configuredApplications = KubernetesConfigurationHelper.getApplicationConfigurations(applications, configurations);
|
|
||||||
const { helmApplications, nonHelmApplications } = KubernetesApplicationHelper.getNestedApplications(configuredApplications);
|
|
||||||
|
|
||||||
this.state.applications = [...helmApplications, ...nonHelmApplications];
|
|
||||||
this.state.stacks = KubernetesStackHelper.stacksFromApplications(applications);
|
|
||||||
this.state.ports = KubernetesApplicationHelper.portMappingsFromApplications(applications);
|
|
||||||
|
|
||||||
this.$scope.$apply();
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
|
||||||
} finally {
|
|
||||||
this.state.isAppsLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSystemResources(flag) {
|
setSystemResources(flag) {
|
||||||
return this.$scope.$applyAsync(() => {
|
return this.$scope.$applyAsync(() => {
|
||||||
this.state.isSystemResources = flag;
|
this.state.isSystemResources = flag;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getApplications() {
|
|
||||||
return this.$async(this.getApplicationsAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onInit() {
|
async onInit() {
|
||||||
this.state = {
|
this.state = {
|
||||||
activeTab: this.LocalStorage.getActiveTab('applications'),
|
activeTab: this.LocalStorage.getActiveTab('applications'),
|
||||||
|
@ -190,12 +156,12 @@ class KubernetesApplicationsController {
|
||||||
this.deploymentOptions = await getDeploymentOptions();
|
this.deploymentOptions = await getDeploymentOptions();
|
||||||
|
|
||||||
this.user = this.Authentication.getUserDetails();
|
this.user = this.Authentication.getUserDetails();
|
||||||
this.state.namespaces = await this.KubernetesNamespaceService.get();
|
this.state.namespaces = await getNamespaces(this.endpoint.Id);
|
||||||
|
|
||||||
const savedNamespace = this.LocalStorage.getNamespaceFilter(this.endpoint.Id, this.user.ID); // could be null if not found, and '' if all namepsaces is selected
|
const savedNamespace = this.LocalStorage.getNamespaceFilter(this.endpoint.Id, this.user.ID); // could be null if not found, and '' if all namepsaces is selected
|
||||||
const preferredNamespace = savedNamespace === null ? 'default' : savedNamespace;
|
const preferredNamespace = savedNamespace === null ? 'default' : savedNamespace;
|
||||||
|
|
||||||
this.state.namespaces = this.state.namespaces.filter((n) => n.Status === 'Active');
|
this.state.namespaces = this.state.namespaces.filter((n) => n.Status.phase === 'Active');
|
||||||
this.state.namespaces = _.sortBy(this.state.namespaces, 'Name');
|
this.state.namespaces = _.sortBy(this.state.namespaces, 'Name');
|
||||||
// set all namespaces ('') if there are no namespaces, or if all namespaces is selected
|
// set all namespaces ('') if there are no namespaces, or if all namespaces is selected
|
||||||
if (!this.state.namespaces.length || preferredNamespace === '') {
|
if (!this.state.namespaces.length || preferredNamespace === '') {
|
||||||
|
@ -205,8 +171,6 @@ class KubernetesApplicationsController {
|
||||||
this.state.namespaceName = this.state.namespaces.find((n) => n.Name === preferredNamespace) ? preferredNamespace : this.state.namespaces[0].Name;
|
this.state.namespaceName = this.state.namespaces.find((n) => n.Name === preferredNamespace) ? preferredNamespace : this.state.namespaces[0].Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.getApplications();
|
|
||||||
|
|
||||||
this.state.viewReady = true;
|
this.state.viewReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -918,7 +918,7 @@ class KubernetesCreateApplicationController {
|
||||||
async checkIngressesToUpdate() {
|
async checkIngressesToUpdate() {
|
||||||
let ingressesToUpdate = [];
|
let ingressesToUpdate = [];
|
||||||
let servicePortsToUpdate = [];
|
let servicePortsToUpdate = [];
|
||||||
const fullIngresses = await getIngresses(this.endpoint.Id, this.formValues.ResourcePool.Namespace.Name);
|
const fullIngresses = await getIngresses(this.endpoint.Id);
|
||||||
this.formValues.Services.forEach((updatedService) => {
|
this.formValues.Services.forEach((updatedService) => {
|
||||||
const oldServiceIndex = this.oldFormValues.Services.findIndex((oldService) => oldService.Name === updatedService.Name);
|
const oldServiceIndex = this.oldFormValues.Services.findIndex((oldService) => oldService.Name === updatedService.Name);
|
||||||
const numberOfPortsInOldService = this.oldFormValues.Services[oldServiceIndex] && this.oldFormValues.Services[oldServiceIndex].Ports.length;
|
const numberOfPortsInOldService = this.oldFormValues.Services[oldServiceIndex] && this.oldFormValues.Services[oldServiceIndex].Ports.length;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
import KubernetesPodConverter from 'Kubernetes/pod/converter';
|
import KubernetesPodConverter from 'Kubernetes/pod/converter';
|
||||||
import { getMetricsForPod } from '@/react/kubernetes/services/service.ts';
|
import { getMetricsForPod } from '@/react/kubernetes/metrics/metrics.ts';
|
||||||
|
|
||||||
class KubernetesApplicationStatsController {
|
class KubernetesApplicationStatsController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
|
|
@ -3,7 +3,7 @@ import _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
||||||
import { getMetricsForAllNodes } from '@/react/kubernetes/services/service.ts';
|
import { getMetricsForAllNodes, getTotalResourcesForAllApplications } from '@/react/kubernetes/metrics/metrics.ts';
|
||||||
|
|
||||||
class KubernetesClusterController {
|
class KubernetesClusterController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -68,20 +68,11 @@ class KubernetesClusterController {
|
||||||
async getApplicationsAsync() {
|
async getApplicationsAsync() {
|
||||||
try {
|
try {
|
||||||
this.state.applicationsLoading = true;
|
this.state.applicationsLoading = true;
|
||||||
this.applications = await this.KubernetesApplicationService.get();
|
|
||||||
const nodeNames = _.map(this.nodes, (node) => node.Name);
|
const applicationsResources = await getTotalResourcesForAllApplications(this.endpoint.Id);
|
||||||
this.resourceReservation = _.reduce(
|
this.resourceReservation = new KubernetesResourceReservation();
|
||||||
this.applications,
|
this.resourceReservation.CPU = Math.round(applicationsResources.CpuRequest / 1000);
|
||||||
(acc, app) => {
|
this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(applicationsResources.MemoryRequest);
|
||||||
app.Pods = _.filter(app.Pods, (pod) => nodeNames.includes(pod.Node));
|
|
||||||
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
|
|
||||||
acc.CPU += resourceReservation.CPU;
|
|
||||||
acc.Memory += resourceReservation.Memory;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
new KubernetesResourceReservation()
|
|
||||||
);
|
|
||||||
this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.Memory);
|
|
||||||
|
|
||||||
if (this.hasResourceUsageAccess()) {
|
if (this.hasResourceUsageAccess()) {
|
||||||
await this.getResourceUsage(this.endpoint.Id);
|
await this.getResourceUsage(this.endpoint.Id);
|
||||||
|
@ -133,8 +124,7 @@ class KubernetesClusterController {
|
||||||
|
|
||||||
await this.getNodes();
|
await this.getNodes();
|
||||||
if (this.isAdmin) {
|
if (this.isAdmin) {
|
||||||
await this.getEndpoints();
|
await Promise.allSettled([this.getEndpoints(), this.getApplicationsAsync()]);
|
||||||
await this.getApplications();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.viewReady = true;
|
this.state.viewReady = true;
|
||||||
|
|
|
@ -76,12 +76,12 @@
|
||||||
<div style="padding: 8px">
|
<div style="padding: 8px">
|
||||||
<kubernetes-resource-reservation
|
<kubernetes-resource-reservation
|
||||||
ng-if="ctrl.resourceReservation"
|
ng-if="ctrl.resourceReservation"
|
||||||
cpu-reservation="ctrl.resourceReservation.CPU"
|
cpu-reservation="ctrl.resourceReservation.CpuRequest"
|
||||||
cpu-usage="ctrl.resourceUsage.CPU"
|
cpu-usage="ctrl.resourceUsage.CPU"
|
||||||
cpu-limit="ctrl.node.CPU"
|
cpu-limit="ctrl.node.CPU"
|
||||||
memory-reservation="ctrl.resourceReservation.Memory"
|
memory-reservation="ctrl.resourceReservation.MemoryRequest"
|
||||||
memory-usage="ctrl.resourceUsage.Memory"
|
memory-usage="ctrl.resourceUsage.Memory"
|
||||||
memory-limit="ctrl.memoryLimit"
|
memory-limit="ctrl.node.Memory"
|
||||||
description="Resource reservation represents the total amount of resource assigned to all the applications running on this node."
|
description="Resource reservation represents the total amount of resource assigned to all the applications running on this node."
|
||||||
display-usage="ctrl.hasResourceUsageAccess()"
|
display-usage="ctrl.hasResourceUsageAccess()"
|
||||||
>
|
>
|
||||||
|
@ -267,11 +267,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<kubernetes-node-applications-datatable
|
<kubernetes-node-applications-datatable></kubernetes-node-applications-datatable>
|
||||||
ng-if="ctrl.applications && ctrl.applications.length > 0"
|
|
||||||
dataset="ctrl.applications"
|
|
||||||
on-refresh="(ctrl.getApplications)"
|
|
||||||
is-loading="ctrl.state.applicationsLoading"
|
|
||||||
>
|
|
||||||
</kubernetes-node-applications-datatable>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { KubernetesNodeTaintEffects, KubernetesNodeAvailabilities } from 'Kubern
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||||
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
|
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
|
||||||
import { confirmUpdateNode } from '@/react/kubernetes/cluster/NodeView/ConfirmUpdateNode';
|
import { confirmUpdateNode } from '@/react/kubernetes/cluster/NodeView/ConfirmUpdateNode';
|
||||||
import { getMetricsForNode } from '@/react/kubernetes/services/service.ts';
|
import { getMetricsForNode, getTotalResourcesForAllApplications } from '@/react/kubernetes/metrics/metrics.ts';
|
||||||
|
|
||||||
class KubernetesNodeController {
|
class KubernetesNodeController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -40,7 +40,6 @@ class KubernetesNodeController {
|
||||||
this.getNodesAsync = this.getNodesAsync.bind(this);
|
this.getNodesAsync = this.getNodesAsync.bind(this);
|
||||||
this.getEvents = this.getEvents.bind(this);
|
this.getEvents = this.getEvents.bind(this);
|
||||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
|
||||||
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
|
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
|
||||||
this.updateNodeAsync = this.updateNodeAsync.bind(this);
|
this.updateNodeAsync = this.updateNodeAsync.bind(this);
|
||||||
this.drainNodeAsync = this.drainNodeAsync.bind(this);
|
this.drainNodeAsync = this.drainNodeAsync.bind(this);
|
||||||
|
@ -300,6 +299,8 @@ class KubernetesNodeController {
|
||||||
try {
|
try {
|
||||||
const nodeName = this.$transition$.params().nodeName;
|
const nodeName = this.$transition$.params().nodeName;
|
||||||
const node = await getMetricsForNode(this.$state.params.endpointId, nodeName);
|
const node = await getMetricsForNode(this.$state.params.endpointId, nodeName);
|
||||||
|
node.CPU = node.usage.cpu;
|
||||||
|
node.Memory = KubernetesResourceReservationHelper.megaBytesValue(node.usage.memory);
|
||||||
this.resourceUsage = new KubernetesResourceReservation();
|
this.resourceUsage = new KubernetesResourceReservation();
|
||||||
this.resourceUsage.CPU = KubernetesResourceReservationHelper.parseCPU(node.usage.cpu);
|
this.resourceUsage.CPU = KubernetesResourceReservationHelper.parseCPU(node.usage.cpu);
|
||||||
this.resourceUsage.Memory = KubernetesResourceReservationHelper.megaBytesValue(node.usage.memory);
|
this.resourceUsage.Memory = KubernetesResourceReservationHelper.megaBytesValue(node.usage.memory);
|
||||||
|
@ -338,43 +339,6 @@ class KubernetesNodeController {
|
||||||
this.selectTab(2);
|
this.selectTab(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApplicationsAsync() {
|
|
||||||
try {
|
|
||||||
this.state.applicationsLoading = true;
|
|
||||||
this.applications = await this.KubernetesApplicationService.get();
|
|
||||||
|
|
||||||
this.resourceReservation = new KubernetesResourceReservation();
|
|
||||||
this.applications = _.map(this.applications, (app) => {
|
|
||||||
app.Pods = _.filter(app.Pods, (pod) => pod.Node === this.node.Name);
|
|
||||||
return app;
|
|
||||||
});
|
|
||||||
this.applications = _.filter(this.applications, (app) => app.Pods.length !== 0);
|
|
||||||
this.applications = _.map(this.applications, (app) => {
|
|
||||||
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
|
|
||||||
app.CPU = resourceReservation.CPU;
|
|
||||||
app.Memory = resourceReservation.Memory;
|
|
||||||
this.resourceReservation.CPU += resourceReservation.CPU;
|
|
||||||
this.resourceReservation.Memory += resourceReservation.Memory;
|
|
||||||
return app;
|
|
||||||
});
|
|
||||||
this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.Memory);
|
|
||||||
this.memoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.node.Memory);
|
|
||||||
this.state.isContainPortainer = _.find(this.applications, { ApplicationName: 'portainer' });
|
|
||||||
|
|
||||||
if (this.hasResourceUsageAccess()) {
|
|
||||||
await this.getNodeUsage();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
|
||||||
} finally {
|
|
||||||
this.state.applicationsLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getApplications() {
|
|
||||||
return this.$async(this.getApplicationsAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onInit() {
|
async onInit() {
|
||||||
this.availabilities = KubernetesNodeAvailabilities;
|
this.availabilities = KubernetesNodeAvailabilities;
|
||||||
|
|
||||||
|
@ -399,7 +363,6 @@ class KubernetesNodeController {
|
||||||
|
|
||||||
await this.getNodes();
|
await this.getNodes();
|
||||||
await this.getEvents();
|
await this.getEvents();
|
||||||
await this.getApplications();
|
|
||||||
await this.getEndpoints();
|
await this.getEndpoints();
|
||||||
|
|
||||||
this.availableEffects = _.values(KubernetesNodeTaintEffects);
|
this.availableEffects = _.values(KubernetesNodeTaintEffects);
|
||||||
|
@ -407,6 +370,11 @@ class KubernetesNodeController {
|
||||||
this.formValues.Labels = KubernetesNodeHelper.computeUsedLabels(this.applications, this.formValues.Labels);
|
this.formValues.Labels = KubernetesNodeHelper.computeUsedLabels(this.applications, this.formValues.Labels);
|
||||||
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
|
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
|
||||||
|
|
||||||
|
this.resourceReservation = await getTotalResourcesForAllApplications(this.$state.params.endpointId, this.node.Name);
|
||||||
|
this.resourceReservation.CpuRequest = Math.round(this.resourceReservation.CpuRequest / 1000);
|
||||||
|
this.resourceReservation.MemoryRequest = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.MemoryRequest);
|
||||||
|
this.node.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.node.Memory);
|
||||||
|
|
||||||
this.state.viewReady = true;
|
this.state.viewReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import moment from 'moment';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
import { PORTAINER_FADEOUT } from '@/constants';
|
import { PORTAINER_FADEOUT } from '@/constants';
|
||||||
import { getMetricsForNode } from '@/react/kubernetes/services/service.ts';
|
import { getMetricsForNode } from '@/react/kubernetes/metrics/metrics.ts';
|
||||||
|
|
||||||
class KubernetesNodeStatsController {
|
class KubernetesNodeStatsController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue