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>
pull/12273/head
Steven Kang 2024-10-01 14:15:51 +13:00 committed by GitHub
parent da010f3d08
commit ea228c3d6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
276 changed files with 9241 additions and 3361 deletions

View File

@ -9,8 +9,6 @@ linters:
- gosimple
- govet
- errorlint
- copyloopvar
- intrange
linters-settings:
depguard:

View File

@ -90,7 +90,7 @@ func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endp
switch {
case endpointutils.IsKubernetesEndpoint(environment):
// 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 {
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
return err

View File

@ -44,7 +44,7 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
}
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 {
return "", err
}

View File

@ -128,7 +128,7 @@ func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, name
return true, nil
}
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
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) {
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return nil, err
}

View File

@ -134,7 +134,7 @@ func (handler *Handler) updateKubeAccess(endpoint *portainer.Endpoint, registry
namespacesToRemove := setDifference(oldNamespacesSet, newNamespacesSet)
namespacesToAdd := setDifference(newNamespacesSet, oldNamespacesSet)
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
cli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return err
}

View 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
}

View 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
}

View 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)
}

View 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)
}

View File

@ -12,45 +12,48 @@ import (
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"
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
)
// @id GetKubernetesConfig
// @summary Generate a kubeconfig file enabling client communication with k8s api server
// @description Generate a kubeconfig file enabling client communication with k8s api server
// @description **Access policy**: authenticated
// @summary Generate a kubeconfig file
// @description Generate a kubeconfig file that allows a client to communicate with the Kubernetes API server
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @security ApiKeyAuth || jwt
// @produce application/json, application/yaml
// @param ids query []int false "will include only these environments(endpoints)"
// @param excludeIds query []int false "will exclude these environments(endpoints)"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 401 "Unauthorized"
// @failure 403 "Permission denied"
// @failure 404 "Environment(Endpoint) or ServiceAccount not found"
// @failure 500 "Server error"
// @success 200 {object} interface{} "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 generate the kubeconfig file."
// @router /kubernetes/config [get]
func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
tokenData, err := security.RetrieveTokenData(r)
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)
}
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
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)
}
endpoints, handlerErr := handler.filterUserKubeEndpoints(r)
if handlerErr != nil {
log.Error().Err(handlerErr).Str("context", "getKubernetesConfig").Msg("Unable to filter user kube endpoints")
return handlerErr
}
if len(endpoints) == 0 {
log.Error().Str("context", "getKubernetesConfig").Msg("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)
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"))
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
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)
}
endpointGroups, err := handler.DataStore.EndpointGroup().ReadAll()
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)
}
@ -85,6 +91,7 @@ func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.En
for _, endpointID := range endpointIDs {
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
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)
}
if !endpointutils.IsKubernetesEndpoint(endpoint) {
@ -101,6 +108,7 @@ func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.En
var kubeEndpoints []portainer.Endpoint
endpoints, err := handler.DataStore.Endpoint().Endpoints()
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)
}
@ -197,6 +205,7 @@ func writeFileContent(w http.ResponseWriter, r *http.Request, endpoints []portai
if r.Header.Get("Accept") == "text/yaml" {
yaml, err := kcli.GenerateYAML(config)
if err != nil {
log.Error().Err(err).Str("context", "writeFileContent").Msg("Failed to generate Kubeconfig")
return httperror.InternalServerError("Failed to generate Kubeconfig", err)
}

View 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
}

View File

@ -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)
}

View File

@ -9,11 +9,10 @@ import (
// @id GetKubernetesDashboard
// @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 **Access policy**: authenticated
// @description Get the dashboard summary data which is simply a count of a range of different commonly used kubernetes resources.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"

View File

@ -1,7 +1,7 @@
package kubernetes
import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
@ -11,11 +11,11 @@ import (
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"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/cli"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/rs/zerolog/log"
"github.com/gorilla/mux"
)
@ -49,94 +49,98 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
// endpoints
endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter()
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
endpointRouter.Use(kubeOnlyMiddleware)
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("/nodes_limits", httperror.LoggerHandler(h.getKubernetesNodesLimits)).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/{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}/{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("/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("/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.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("/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("/volumes", httperror.LoggerHandler(h.GetAllKubernetesVolumes)).Methods(http.MethodGet)
endpointRouter.Handle("/volumes/count", httperror.LoggerHandler(h.getAllKubernetesVolumesCount)).Methods(http.MethodGet)
// namespaces
// 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.
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("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
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.updateKubernetesIngress)).Methods(http.MethodPut)
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.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
}
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
// 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.
func (h *Handler) getProxyKubeClient(r *http.Request) (*cli.KubeClient, *httperror.HandlerError) {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
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)
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)
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
}
// 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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(portainer.PortainerCacheHeader, "true")
if handler.KubernetesClientFactory == nil {
next.ServeHTTP(w, r)
return
@ -144,13 +148,13 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
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
}
tokenData, err := security.RetrieveTokenData(r)
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
@ -163,35 +167,60 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err != nil {
if handler.DataStore.IsErrObjectNotFound(err) {
httperror.WriteError(
w,
http.StatusNotFound,
"Unable to find an environment with the specified identifier inside the database",
err,
)
httperror.WriteError(w, http.StatusNotFound,
"an error occurred during the KubeClientMiddleware operation, unable to find an environment with the specified environment identifier inside the database. Error: ", err)
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
}
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)
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
}
config := handler.buildConfig(r, tokenData, bearerToken, []portainer.Endpoint{*endpoint}, true)
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
}
// Manually setting serverURL to localhost to route the request to proxy server
serverURL, err := url.Parse(config.Clusters[0].Cluster.Server)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable parse cluster's kubeconfig server URL", nil)
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to parse server URL for building kubeconfig. Error: ", err)
return
}
serverURL.Scheme = "https"
@ -200,17 +229,12 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
yaml, err := cli.GenerateYAML(config)
if err != nil {
httperror.WriteError(
w,
http.StatusInternalServerError,
"Unable to generate yaml from endpoint kubeconfig",
err,
)
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to generate kubeconfig YAML. Error: ", err)
return
}
kubeCli, err := handler.KubernetesClientFactory.CreateKubeClientFromKubeConfig(endpoint.Name, []byte(yaml))
kubeCli, err := handler.KubernetesClientFactory.CreateKubeClientFromKubeConfig(endpoint.Name, []byte(yaml), isKubeAdmin, nonAdminNamespaces)
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
}

View File

@ -10,67 +10,65 @@ import (
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 getKubernetesIngressControllers
// @id GetAllKubernetesIngressControllers
// @summary Get a list of ingress controllers
// @description Get a list of ingress controllers for the given environment
// @description **Access policy**: authenticated
// @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 user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @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"
// @success 200 {object} models.K8sIngressControllers "Success"
// @failure 400 "Invalid request"
// @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 ingress controllers"
// @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")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Invalid environment identifier route variable")
return httperror.BadRequest("Invalid environment identifier route variable", err)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return httperror.NotFound(
"Unable to find an environment with the specified identifier inside the database",
err,
)
} else if err != nil {
return httperror.InternalServerError(
"Unable to find an environment with the specified identifier inside the database",
err,
)
if err != nil {
if handler.DataStore.IsErrObjectNotFound(err) {
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("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)
}
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").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)
}
allowedOnly, err := request.RetrieveBooleanQueryParameter(r, "allowedOnly", true)
if err != nil {
return httperror.BadRequest(
"Invalid allowedOnly boolean query parameter",
err,
)
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to retrieve allowedOnly query parameter")
return httperror.BadRequest("Unable to retrieve allowedOnly query parameter", err)
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError(
"Unable to create Kubernetes client",
err,
)
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to get privileged kube client")
return httperror.InternalServerError("Unable to get privileged kube client", err)
}
controllers, err := cli.GetIngressControllers()
if err != nil {
return httperror.InternalServerError(
"Failed to fetch ingressclasses",
err,
)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unauthorized access to the Kubernetes API")
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.
@ -82,16 +80,17 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
})
}
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
var updatedClasses []portainer.KubernetesIngressClassConfig
updatedClasses := []portainer.KubernetesIngressClassConfig{}
for i := range controllers {
controllers[i].Availability = true
if controllers[i].ClassName != "none" {
controllers[i].New = true
}
var updatedClass portainer.KubernetesIngressClassConfig
updatedClass.Name = controllers[i].ClassName
updatedClass.Type = controllers[i].Type
updatedClass := portainer.KubernetesIngressClassConfig{
Name: controllers[i].ClassName,
Type: controllers[i].Type,
}
// Check if the controller is already known.
for _, existingClass := range existingClasses {
@ -112,16 +111,14 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
endpoint,
)
if err != nil {
return httperror.InternalServerError(
"Unable to store found IngressClasses inside the database",
err,
)
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to store found IngressClasses inside the database")
return httperror.InternalServerError("Unable to store found IngressClasses inside the database", err)
}
// If the allowedOnly query parameter was set. We need to prune out
// disallowed controllers from the response.
if allowedOnly {
var allowedControllers models.K8sIngressControllers
allowedControllers := models.K8sIngressControllers{}
for _, controller := range controllers {
if controller.Availability {
allowedControllers = append(allowedControllers, controller)
@ -132,62 +129,61 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
return response.JSON(w, controllers)
}
// @id getKubernetesIngressControllersByNamespace
// @id GetKubernetesIngressControllersByNamespace
// @summary Get a list ingress controllers by namespace
// @description Get a list of ingress controllers for the given environment in the provided namespace
// @description **Access policy**: authenticated
// @description Get a list of ingress controllers for the given environment in the provided namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @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 {object} models.K8sIngressControllers "Success"
// @failure 400 "Invalid request"
// @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 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]
func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to retrieve environment identifier from request")
return httperror.BadRequest("Unable to retrieve environment identifier from request", err)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return httperror.NotFound(
"Unable to find an environment with the specified identifier inside the database",
err,
)
} else if err != nil {
return httperror.InternalServerError(
"Unable to find an environment with the specified identifier inside the database",
err,
)
if err != nil {
if handler.DataStore.IsErrObjectNotFound(err) {
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("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)
}
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").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)
}
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")
if err != nil {
return httperror.BadRequest(
"Invalid namespace identifier route variable",
err,
)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to retrieve namespace from request")
return httperror.BadRequest("Unable to retrieve namespace from request", err)
}
currentControllers, err := cli.GetIngressControllers()
if err != nil {
return httperror.InternalServerError(
"Failed to fetch ingressclasses",
err,
)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").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", "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.
if endpoint.Kubernetes.Configuration.AllowNoneIngressClass {
@ -197,21 +193,24 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon
Type: "custom",
})
}
kubernetesConfig := endpoint.Kubernetes.Configuration
existingClasses := kubernetesConfig.IngressClasses
ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace
var updatedClasses []portainer.KubernetesIngressClassConfig
var controllers models.K8sIngressControllers
updatedClasses := []portainer.KubernetesIngressClassConfig{}
controllers := models.K8sIngressControllers{}
for i := range currentControllers {
var globallyblocked bool
globallyblocked := false
currentControllers[i].Availability = true
if currentControllers[i].ClassName != "none" {
currentControllers[i].New = true
}
var updatedClass portainer.KubernetesIngressClassConfig
updatedClass.Name = currentControllers[i].ClassName
updatedClass.Type = currentControllers[i].Type
updatedClass := portainer.KubernetesIngressClassConfig{
Name: currentControllers[i].ClassName,
Type: currentControllers[i].Type,
}
// Check if the controller is blocked globally or in the current
// namespace.
@ -243,81 +242,77 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon
// Update the database to match the list of found controllers.
// This includes pruning out controllers which no longer exist.
endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
err = handler.DataStore.Endpoint().UpdateEndpoint(
portainer.EndpointID(endpointID),
endpoint,
)
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
if err != nil {
return httperror.InternalServerError(
"Unable to store found IngressClasses inside the database",
err,
)
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to store found IngressClasses inside the database")
return httperror.InternalServerError("Unable to store found IngressClasses inside the database", err)
}
return response.JSON(w, controllers)
}
// @id updateKubernetesIngressControllers
// @id UpdateKubernetesIngressControllers
// @summary Update (block/unblock) ingress controllers
// @description Update (block/unblock) ingress controllers
// @description **Access policy**: authenticated
// @description Update (block/unblock) ingress controllers for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param body body []models.K8sIngressControllers true "Ingress controllers"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @param id path int true "Environment identifier"
// @param body body models.K8sIngressControllers true "Ingress controllers"
// @success 204 "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 the ingress controllers to update."
// @failure 500 "Server error occurred while attempting to update ingress controllers."
// @router /kubernetes/{id}/ingresscontrollers [put]
func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to retrieve environment identifier from request")
return httperror.BadRequest("Unable to retrieve environment identifier from request", err)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return httperror.NotFound(
"Unable to find an environment with the specified identifier inside the database",
err,
)
} else if err != nil {
return httperror.InternalServerError(
"Unable to find an environment with the specified identifier inside the database",
err,
)
if err != nil {
if handler.DataStore.IsErrObjectNotFound(err) {
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("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)
}
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)
}
var payload models.K8sIngressControllers
payload := models.K8sIngressControllers{}
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest(
"Invalid request payload",
err,
)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to decode and validate the request payload")
return httperror.BadRequest("Unable to decode and validate the request payload", err)
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError(
"Unable to create Kubernetes client",
err,
)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to get privileged kube client")
return httperror.InternalServerError("Unable to get privileged kube client", err)
}
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
controllers, err := cli.GetIngressControllers()
if err != nil {
return httperror.InternalServerError(
"Unable to get ingress controllers",
err,
)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").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", "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.
@ -329,14 +324,15 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter
})
}
var updatedClasses []portainer.KubernetesIngressClassConfig
updatedClasses := []portainer.KubernetesIngressClassConfig{}
for i := range controllers {
controllers[i].Availability = true
controllers[i].New = true
var updatedClass portainer.KubernetesIngressClassConfig
updatedClass.Name = controllers[i].ClassName
updatedClass.Type = controllers[i].Type
updatedClass := portainer.KubernetesIngressClassConfig{
Name: controllers[i].ClassName,
Type: controllers[i].Type,
}
// Check if the controller is already known.
for _, existingClass := range existingClasses {
@ -366,59 +362,64 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter
endpoint,
)
if err != nil {
return httperror.InternalServerError(
"Unable to update the BlockedIngressClasses inside the database",
err,
)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to store found IngressClasses inside the database")
return httperror.InternalServerError("Unable to store found IngressClasses inside the database", err)
}
return response.Empty(w)
}
// @id updateKubernetesIngressControllersByNamespace
// @id UpdateKubernetesIngressControllersByNamespace
// @summary Update (block/unblock) ingress controllers by namespace
// @description Update (block/unblock) ingress controllers by namespace for the provided environment
// @description **Access policy**: authenticated
// @description Update (block/unblock) ingress controllers by namespace for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept 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 body body []models.K8sIngressControllers true "Ingress controllers"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @param body body models.K8sIngressControllers true "Ingress controllers"
// @success 204 "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 update ingress controllers by namespace."
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [put]
func (handler *Handler) updateKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
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")
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)
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
var updatedClasses []portainer.KubernetesIngressClassConfig
updatedClasses := []portainer.KubernetesIngressClassConfig{}
PayloadLoop:
for _, p := range payload {
for _, existingClass := range existingClasses {
if p.ClassName != existingClass.Name {
continue
}
var updatedClass portainer.KubernetesIngressClassConfig
updatedClass.Name = existingClass.Name
updatedClass.Type = existingClass.Type
updatedClass.GloballyBlocked = existingClass.GloballyBlocked
updatedClass := portainer.KubernetesIngressClassConfig{
Name: existingClass.Name,
Type: existingClass.Type,
GloballyBlocked: existingClass.GloballyBlocked,
}
// Handle "allow"
if p.Availability {
@ -445,10 +446,7 @@ PayloadLoop:
continue PayloadLoop
}
}
updatedClass.BlockedNamespaces = append(
updatedClass.BlockedNamespaces,
namespace,
)
updatedClass.BlockedNamespaces = append(updatedClass.BlockedNamespaces, namespace)
updatedClasses = append(updatedClasses, updatedClass)
}
}
@ -458,7 +456,7 @@ PayloadLoop:
// part of updatedClasses, but we MUST include it or we would remove the
// global block.
for _, existingClass := range existingClasses {
var found bool
found := false
for _, updatedClass := range updatedClasses {
if existingClass.Name == updatedClass.Name {
@ -474,32 +472,125 @@ PayloadLoop:
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
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)
}
// @id getKubernetesIngresses
// @summary Get kubernetes ingresses by namespace
// @description Get kubernetes ingresses by namespace for the provided environment
// @description **Access policy**: authenticated
// @id GetAllKubernetesClusterIngresses
// @summary Get kubernetes ingresses at the cluster level
// @description Get kubernetes ingresses at the cluster level for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @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 body body []models.K8sIngressInfo true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @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}/namespaces/{namespace}/ingresses [get]
func (handler *Handler) getKubernetesIngresses(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)
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)
@ -509,38 +600,103 @@ func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Re
ingresses, err := cli.GetIngresses(namespace)
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)
}
// @id createKubernetesIngress
// @summary Create a kubernetes ingress by namespace
// @description Create a kubernetes ingress by namespace for the provided environment
// @description **Access policy**: authenticated
// @id GetKubernetesIngress
// @summary Get an Ingress by name
// @description Get an Ingress by name for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || 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
// @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 body body models.K8sIngressInfo true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "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 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]
func (handler *Handler) createKubernetesIngress(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)
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)
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"
@ -556,26 +712,39 @@ func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.R
err = cli.CreateIngress(namespace, payload, owner)
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)
}
// @id deleteKubernetesIngresses
// @summary Delete kubernetes ingresses
// @description Delete kubernetes ingresses for the provided environment
// @description **Access policy**: authenticated
// @id DeleteKubernetesIngresses
// @summary Delete one or more Ingresses
// @description Delete one or more Ingresses in the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept 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"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "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 a specific ingress."
// @failure 500 "Server error occurred while attempting to delete specified ingresses."
// @router /kubernetes/{id}/ingresses/delete [post]
func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, handlerErr := handler.getProxyKubeClient(r)
@ -583,46 +752,62 @@ func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http
return handlerErr
}
var payload models.K8sIngressDeleteRequests
payload := models.K8sIngressDeleteRequests{}
err := request.DecodeAndValidateJSONPayload(r, &payload)
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)
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 response.Empty(w)
}
// @id updateKubernetesIngress
// @summary Update kubernetes ingress rule
// @description Update kubernetes ingress rule for the provided environment
// @description **Access policy**: authenticated
// @id UpdateKubernetesIngress
// @summary Update an Ingress
// @description Update an Ingress for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept 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 body body models.K8sIngressInfo true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "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 the specified ingress."
// @failure 500 "Server error occurred while attempting to update the specified ingress."
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [put]
func (handler *Handler) updateKubernetesIngress(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)
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)
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)
@ -632,7 +817,18 @@ func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.R
err = cli.UpdateIngress(namespace, payload)
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)

View File

@ -7,23 +7,22 @@ import (
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"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// @id getKubernetesMetricsForAllNodes
// @id GetKubernetesMetricsForAllNodes
// @summary Get a list of nodes with their live metrics
// @description Get a list of nodes with their live metrics
// @description **Access policy**: authenticated
// @description Get a list of metrics associated with all nodes of a cluster.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @success 200 {object} v1beta1.NodeMetricsList "Success"
// @failure 400 "Invalid request"
// @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 nodes with their live metrics."
// @router /kubernetes/{id}/metrics/nodes [get]
func (handler *Handler) getKubernetesMetricsForAllNodes(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
@ -33,45 +32,49 @@ func (handler *Handler) getKubernetesMetricsForAllNodes(w http.ResponseWriter, r
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
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)
}
metrics, err := cli.MetricsV1beta1().NodeMetricses().List(r.Context(), v1.ListOptions{})
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllNodes").Msg("Failed to fetch metrics")
return httperror.InternalServerError("Failed to fetch metrics", err)
}
return response.JSON(w, metrics)
}
// @id getKubernetesMetricsForNode
// @id GetKubernetesMetricsForNode
// @summary Get live metrics for a node
// @description Get live metrics for a node
// @description **Access policy**: authenticated
// @description Get live metrics for the specified node.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param name path string true "Node identifier"
// @success 200 {object} v1beta1.NodeMetrics "Success"
// @failure 400 "Invalid request"
// @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 500 "Server error occurred while attempting to retrieve the live metrics for the specified node."
// @router /kubernetes/{id}/metrics/nodes/{name} [get]
func (handler *Handler) getKubernetesMetricsForNode(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Failed to fetch endpoint")
return httperror.InternalServerError(err.Error(), err)
}
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
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)
}
nodeName, err := request.RetrieveRouteVariableValue(r, "name")
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)
}
@ -81,90 +84,98 @@ func (handler *Handler) getKubernetesMetricsForNode(w http.ResponseWriter, r *ht
v1.GetOptions{},
)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Failed to fetch metrics")
return httperror.InternalServerError("Failed to fetch metrics", err)
}
return response.JSON(w, metrics)
}
// @id getKubernetesMetricsForAllPods
// @id GetKubernetesMetricsForAllPods
// @summary Get a list of pods with their live metrics
// @description Get a list of pods with their live metrics
// @description **Access policy**: authenticated
// @description Get a list of pods with their live metrics for the specified namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @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 {object} v1beta1.PodMetricsList "Success"
// @failure 400 "Invalid request"
// @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 500 "Server error occurred while attempting to retrieve the list of pods with their live metrics."
// @router /kubernetes/{id}/metrics/pods/{namespace} [get]
func (handler *Handler) getKubernetesMetricsForAllPods(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Failed to fetch endpoint")
return httperror.InternalServerError(err.Error(), err)
}
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
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)
}
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
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)
}
metrics, err := cli.MetricsV1beta1().PodMetricses(namespace).List(r.Context(), v1.ListOptions{})
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Failed to fetch metrics")
return httperror.InternalServerError("Failed to fetch metrics", err)
}
return response.JSON(w, metrics)
}
// @id getKubernetesMetricsForPod
// @id GetKubernetesMetricsForPod
// @summary Get live metrics for a pod
// @description Get live metrics for a pod
// @description **Access policy**: authenticated
// @description Get live metrics for the specified pod.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace"
// @param name path string true "Pod identifier"
// @success 200 {object} v1beta1.PodMetrics "Success"
// @failure 400 "Invalid request"
// @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 500 "Server error occurred while attempting to retrieve the live metrics for the specified pod."
// @router /kubernetes/{id}/metrics/pods/{namespace}/{name} [get]
func (handler *Handler) getKubernetesMetricsForPod(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Msg("Failed to fetch endpoint")
return httperror.InternalServerError(err.Error(), err)
}
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
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)
}
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
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)
}
podName, err := request.RetrieveRouteVariableValue(r, "name")
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)
}
metrics, err := cli.MetricsV1beta1().PodMetricses(namespace).Get(r.Context(), podName, v1.GetOptions{})
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)
}

View File

@ -1,185 +1,282 @@
package kubernetes
import (
"errors"
"fmt"
"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 getKubernetesNamespaces
// @summary Get a list of kubernetes namespaces
// @description Get a list of all kubernetes namespaces in the cluster
// @description **Access policy**: authenticated
// @id GetKubernetesNamespaces
// @summary Get a list of namespaces
// @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 user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @success 200 {object} map[string]portainer.K8sNamespaceInfo "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @param id path int true "Environment identifier"
// @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 {array} 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 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]
func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
withResourceQuota, err := request.RetrieveBooleanQueryParameter(r, "withResourceQuota", true)
if err != nil {
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()
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
// @summary Get kubernetes namespace details
// @description Get kubernetes namespace details for the provided namespace within the given environment
// @description **Access policy**: authenticated
// @id GetKubernetesNamespacesCount
// @summary Get the total number of kubernetes namespaces within the given Portainer 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 user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace"
// @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 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"
// @failure 400 "Invalid request"
// @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 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]
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 {
return httperror.BadRequest(
"Invalid namespace identifier route variable",
err,
)
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Invalid namespace parameter namespace")
return httperror.BadRequest("an error occurred during the GetKubernetesNamespace operation, invalid namespace parameter namespace. Error: ", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
withResourceQuota, err := request.RetrieveBooleanQueryParameter(r, "withResourceQuota", true)
if err != nil {
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 {
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)
}
// @id createKubernetesNamespace
// @summary Create a kubernetes namespace
// @description Create a kubernetes namespace within the given environment
// @description **Access policy**: authenticated
// @id DeleteKubernetesNamespace
// @summary Delete a kubernetes namespace
// @description Delete a kubernetes namespace within the given environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param body body models.K8sNamespaceDetails true "Namespace configuration details"
// @security ApiKeyAuth || jwt
// @param id path int true "Environment identifier"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces [post]
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
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]
// @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 delete the namespace."
// @router /kubernetes/{id}/namespaces [delete]
func (handler *Handler) deleteKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sNamespaceDetails
err := request.DecodeAndValidateJSONPayload(r, &payload)
namespaceNames, err := request.GetPayload[deleteKubernetesNamespacePayload](r)
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")
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
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)
if handlerErr != nil {
return handlerErr
for _, namespaceName := range *namespaceNames {
_, err := cli.DeleteNamespace(namespaceName)
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)
}
log.Error().Err(err).Str("context", "DeleteKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to delete the namespace")
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)
}
}
err = cli.DeleteNamespace(namespace)
if err != nil {
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
}
// @id updateKubernetesNamespace
// @summary Updates a kubernetes namespace
// @description Update a kubernetes namespace within the given environment
// @description **Access policy**: authenticated
// @id UpdateKubernetesNamespace
// @summary Update a namespace
// @description Update a namespace within the given environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept 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 body body models.K8sNamespaceDetails true "Namespace details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @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 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]
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sNamespaceDetails
payload := models.K8sNamespaceDetails{}
err := request.DecodeAndValidateJSONPayload(r, &payload)
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)
if handlerErr != nil {
return handlerErr
namespaceName := payload.Name
cli, httpErr := handler.getProxyKubeClient(r)
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 {
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)
}

View File

@ -6,53 +6,72 @@ import (
"github.com/portainer/portainer/api/http/middlewares"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id GetKubernetesNodesLimits
// @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 **Access policy**: authenticated
// @description Get CPU and memory limits of all nodes within k8s cluster.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @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"
// @failure 403 "Permission denied"
// @failure 404 "Environment(Endpoint) not found"
// @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 nodes limits."
// @router /kubernetes/{id}/nodes_limits [get]
func (handler *Handler) getKubernetesNodesLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
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)
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
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)
}
nodesLimits, err := cli.GetNodesLimits()
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 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 {
endpoint, err := middlewares.FetchEndpoint(r)
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)
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
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
@ -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
resourceLimit, err := cli.GetMaxResourceLimits("", overCommit, overCommitPercent)
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)
}

View File

@ -5,28 +5,33 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id IsRBACEnabled
// @id GetKubernetesRBACStatus
// @summary Check if RBAC is enabled
// @description Check if RBAC is enabled in the current Kubernetes cluster.
// @description **Access policy**: administrator
// @tags rbac_enabled
// @security ApiKeyAuth
// @security jwt
// @produce text/plain
// @description Check if RBAC is enabled in the specified Kubernetes cluster.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @success 200 "Success"
// @failure 500 "Server error"
// @success 200 {boolean} bool "RBAC status"
// @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]
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)
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()
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)
}

View 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)
}

View 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)
}

View 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
}

View 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)
}

View File

@ -7,165 +7,298 @@ import (
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 getKubernetesServices
// @summary Get a list of kubernetes services for a given namespace
// @description Get a list of kubernetes services for a given namespace
// @description **Access policy**: authenticated
// @id GetKubernetesServices
// @summary Get a list of services
// @description Get a list of services that the user has access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace name"
// @param lookupapplications query boolean false "Lookup applications associated with each service"
// @param id path int true "Environment identifier"
// @param withApplications query boolean false "Lookup applications associated with each service"
// @success 200 {array} models.K8sServiceInfo "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace}/services [get]
func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
// @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."
// @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 {
return httperror.BadRequest("Invalid namespace identifier route variable", 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 err
}
return response.JSON(w, services)
}
// @id createKubernetesService
// @summary Create a kubernetes service
// @description Create a kubernetes service within a given namespace
// @description **Access policy**: authenticated
// @id GetAllKubernetesServicesCount
// @summary Get services count
// @description Get the count of services that the user has access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @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 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
// @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 body body models.K8sServiceInfo true "Service definition"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "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 create a service."
// @router /kubernetes/{id}/namespaces/{namespace}/services [post]
func (handler *Handler) createKubernetesService(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)
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
err = request.DecodeAndValidateJSONPayload(r, &payload)
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)
if handlerErr != nil {
return handlerErr
serviceName := payload.Name
cli, httpError := handler.getProxyKubeClient(r)
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)
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)
}
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 nil
return response.Empty(w)
}
// @id deleteKubernetesServices
// @summary Delete kubernetes services
// @description Delete the provided list of kubernetes services
// @description **Access policy**: authenticated
// @id DeleteKubernetesServices
// @summary Delete services
// @description Delete the provided list of services.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept 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"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "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 a specific service."
// @failure 500 "Server error occurred while attempting to delete services."
// @router /kubernetes/{id}/services/delete [post]
func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sServiceDeleteRequests
payload := models.K8sServiceDeleteRequests{}
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest(
"Invalid request payload",
err,
)
log.Error().Err(err).Str("context", "DeleteKubernetesServices").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)
if handlerErr != nil {
return handlerErr
cli, httpError := handler.getProxyKubeClient(r)
if httpError != nil {
return httpError
}
err = cli.DeleteServices(payload)
if err != nil {
return httperror.InternalServerError(
"Unable to delete service",
err,
)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "DeleteKubernetesServices").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", "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 nil
return response.Empty(w)
}
// @id updateKubernetesService
// @summary Update a kubernetes service
// @description Update a kubernetes service within a given namespace
// @description **Access policy**: authenticated
// @id UpdateKubernetesService
// @summary Update a service
// @description Update a service within a given namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept 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 body body models.K8sServiceInfo true "Service definition"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "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 the service to update."
// @failure 500 "Server error occurred while attempting to update a service."
// @router /kubernetes/{id}/namespaces/{namespace}/services [put]
func (handler *Handler) updateKubernetesService(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)
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
err = request.DecodeAndValidateJSONPayload(r, &payload)
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)
if handlerErr != nil {
return handlerErr
serviceName := payload.Name
cli, httpError := handler.getProxyKubeClient(r)
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)
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

View File

@ -20,19 +20,20 @@ func (payload *namespacesToggleSystemPayload) Validate(r *http.Request) error {
// @id KubernetesNamespacesToggleSystem
// @summary Toggle the system state for a namespace
// @description Toggle the system state for a namespace
// @description **Access policy**: administrator or environment(endpoint) admin
// @security ApiKeyAuth
// @security jwt
// @description Toggle the system state for a namespace
// @description **Access policy**: Administrator or environment administrator.
// @security ApiKeyAuth || jwt
// @tags kubernetes
// @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 body body namespacesToggleSystemPayload true "Update details"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @success 204 "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 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]
func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
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)
}
kubeClient, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
kubeClient, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError("Unable to create kubernetes client", err)
}

View 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
}

View File

@ -135,7 +135,7 @@ func (handler *Handler) userHasRegistryAccess(r *http.Request, registry *portain
// validate access for kubernetes namespaces (leverage registry.RegistryAccesses[endpointId].Namespaces)
if endpointutils.IsKubernetesEndpoint(endpoint) {
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return false, false, errors.Wrap(err, "unable to retrieve kubernetes client to validate registry access")
}

View File

@ -41,7 +41,7 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
if useAuthentication {
username, err := request.RetrieveMultiPartFormValue(r, "Username", false)
if err != nil {
return errors.New("Invalid username")
return errors.New("invalid username")
}
payload.Username = username
@ -61,19 +61,19 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
if useTLS && !skipTLSVerify {
cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile")
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
key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile")
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
ca, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile")
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
}

View File

@ -45,15 +45,15 @@ type registryCreatePayload struct {
func (payload *registryCreatePayload) Validate(_ *http.Request) error {
if len(payload.Name) == 0 {
return errors.New("Invalid registry name")
return errors.New("invalid registry name")
}
if len(payload.URL) == 0 {
return errors.New("Invalid registry URL")
return errors.New("invalid registry URL")
}
if payload.Authentication {
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 len(payload.Ecr.Region) == 0 {
@ -127,10 +127,10 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
}
for _, r := range registries {
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) {
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"))
}
}

View File

@ -67,7 +67,7 @@ func (handler *Handler) deleteKubernetesSecrets(registry *portainer.Registry) {
continue
}
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
cli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
// Skip environments that can't get a kubeclient from
log.Warn().Err(err).Msgf("Unable to get kubernetes client for environment %d", endpointId)

View File

@ -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
for _, r := range registries {
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 {
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 {
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
cli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return err
}

View File

@ -163,7 +163,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
// Refresh ECR registry secret if needed
// RefreshEcrSecret method checks if the namespace has any ECR registry
// otherwise return nil
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err == nil {
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, payload.Namespace)
}

View File

@ -1,11 +1,11 @@
package stacks
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"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)
}
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))
}

View File

@ -124,7 +124,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
// Refresh ECR registry secret if needed
// RefreshEcrSecret method checks if the namespace has any ECR registry
// otherwise return nil
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err == nil {
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, stack.Namespace)
}

View File

@ -47,7 +47,7 @@ func (handler *Handler) updateUserServiceAccounts(membership *portainer.TeamMemb
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
// update kubernenets service accounts if the team is associated with a kubernetes environment
if endpointutils.IsKubernetesEndpoint(&endpoint) {
kubecli, err := handler.K8sClientFactory.GetKubeClient(&endpoint)
kubecli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(&endpoint)
if err != nil {
log.Error().Err(err).Msgf("failed getting kube client for environment %d", endpoint.ID)
continue

View File

@ -102,7 +102,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
return nil
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
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
}
kubecli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
kubecli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return "", false, err
}

View File

@ -42,12 +42,12 @@ func (handler *Handler) websocketShellPodExec(w http.ResponseWriter, r *http.Req
return httperror.Forbidden("Permission denied to access environment", err)
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError("Unable to create Kubernetes client", err)
}
serviceAccount, err := cli.GetServiceAccount(tokenData)
serviceAccount, err := cli.GetPortainerUserServiceAccount(tokenData)
if err != nil {
return httperror.InternalServerError("Unable to find serviceaccount associated with user", err)
}

View File

@ -1,11 +1,77 @@
package kubernetes
type (
K8sApplication struct {
UID string `json:",omitempty"`
Name string `json:""`
Namespace string `json:",omitempty"`
Kind string `json:",omitempty"`
Labels map[string]string `json:",omitempty"`
}
import (
"time"
corev1 "k8s.io/api/core/v1"
)
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"`
}

View 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"`
}
)

View File

@ -0,0 +1,8 @@
package kubernetes
import "time"
type K8sClusterRole struct {
Name string `json:"name"`
CreationDate time.Time `json:"creationDate"`
}

View File

@ -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"`
}
)

View 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"`
}
)

View File

@ -44,6 +44,7 @@ type (
Port int `json:"Port"`
Path string `json:"Path"`
PathType string `json:"PathType"`
HasService bool `json:"HasService"`
}
// K8sIngressDeleteRequests is a mapping of namespace names to a slice of

View 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"`
}
)

View 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"`
}

View 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"`
}

View File

@ -7,16 +7,16 @@ import (
type (
K8sServiceInfo struct {
Name string
UID string
Type string
Namespace string
Annotations map[string]string
CreationTimestamp string
Labels map[string]string
AllocateLoadBalancerNodePorts *bool `json:",omitempty"`
Ports []K8sServicePort
Selector map[string]string
Name string `json:",omitempty"`
UID string `json:",omitempty"`
Type string `json:",omitempty"`
Namespace string `json:",omitempty"`
Annotations map[string]string `json:",omitempty"`
CreationDate string `json:",omitempty"`
Labels map[string]string `json:",omitempty"`
AllocateLoadBalancerNodePorts *bool `json:",omitempty"`
Ports []K8sServicePort `json:",omitempty"`
Selector map[string]string `json:",omitempty"`
IngressStatus []K8sServiceIngress `json:",omitempty"`
// serviceList screen

View 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"`
}
)

View File

@ -27,7 +27,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
return nil, err
}
kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint)
kubecli, err := factory.kubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return nil, err
}
@ -61,7 +61,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
return nil, err
}
kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint)
kubecli, err := factory.kubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return nil, err
}
@ -88,7 +88,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
remoteURL.Scheme = "https"
kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint)
kubecli, err := factory.kubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return nil, err
}

View File

@ -7,7 +7,7 @@ import (
)
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 {
return
}

View File

@ -142,7 +142,7 @@ func (transport *baseTransport) getRoundTripToken(request *http.Request, tokenMa
} else {
token, err = tokenManager.GetUserServiceAccountToken(int(tokenData.ID), transport.endpoint.ID)
if err != nil {
log.Debug().
log.Error().
Err(err).
Msg("failed retrieving service account token")

View File

@ -11,7 +11,7 @@ func (service *Service) CleanNAPWithOverridePolicies(
endpoint *portainer.Endpoint,
endpointGroup *portainer.EndpointGroup,
) error {
kubecli, err := service.K8sClientFactory.GetKubeClient(endpoint)
kubecli, err := service.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return err
}

View File

@ -91,7 +91,7 @@ func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService
}
}()
cli, err := factory.GetKubeClient(endpoint)
cli, err := factory.GetPrivilegedKubeClient(endpoint)
if err != nil {
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 {
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 {
log.Debug().Err(err).Msg("unable to create Kubernetes client for initial storage detection")

View File

@ -2,6 +2,7 @@ package cli
import (
"context"
"fmt"
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")
}
delete(policies, ns)
if policies != nil {
delete(policies, ns)
return kcl.UpdateNamespaceAccessPolicies(policies)
}
return kcl.UpdateNamespaceAccessPolicies(policies)
return nil
}
// GetNamespaceAccessPolicies gets the namespace access policies
// from config maps in the portainer namespace
func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) {
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(context.TODO(), portainerConfigMapName, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
return nil, nil
} else if err != nil {
if err != nil {
if k8serrors.IsNotFound(err) {
return nil, nil
}
return nil, err
}
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
var policies map[string]portainer.K8sNamespaceAccessPolicy
policies := map[string]portainer.K8sNamespaceAccessPolicy{}
err = json.Unmarshal([]byte(accessData), &policies)
if err != nil {
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{})
if k8serrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
@ -122,3 +122,20 @@ func (kcl *KubeClient) UpdateNamespaceAccessPolicies(accessPolicies map[string]p
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
}

View File

@ -2,153 +2,440 @@ package cli
import (
"context"
"strings"
"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"
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
func (kcl *KubeClient) GetApplications(namespace, kind string) ([]models.K8sApplication, error) {
applicationList := []models.K8sApplication{}
listOpts := metav1.ListOptions{}
// GetAllKubernetesApplications gets a list of kubernetes workloads (or applications) across all namespaces in the cluster
// if the user is an admin, all namespaces in the current k8s environment(endpoint) are fetched using the fetchApplications function.
// otherwise, namespaces the non-admin user has access to will be used to filter the applications based on the allowed namespaces.
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") {
deployments, err := kcl.cli.AppsV1().Deployments(namespace).List(context.TODO(), listOpts)
return kcl.fetchApplicationsForNonAdmin(namespace, nodeName, withDependencies)
}
// 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 {
return nil, err
}
for _, d := range deployments.Items {
applicationList = append(applicationList, models.K8sApplication{
UID: string(d.UID),
Name: d.Name,
Namespace: d.Namespace,
Kind: "Deployment",
Labels: d.Labels,
})
}
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, nil)
}
if kind == "" || strings.EqualFold(kind, "statefulset") {
statefulSets, err := kcl.cli.AppsV1().StatefulSets(namespace).List(context.TODO(), listOpts)
pods, replicaSets, deployments, statefulSets, daemonSets, services, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
if err != nil {
return nil, err
}
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services)
}
// fetchApplicationsForNonAdmin fetches the applications in the namespaces the user has access to.
// This function is called when the user is not an admin.
func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string, withDependencies bool) ([]models.K8sApplication, error) {
log.Debug().Msgf("Fetching applications for non-admin user: %v", kcl.NonAdminNamespaces)
if len(kcl.NonAdminNamespaces) == 0 {
return nil, nil
}
podListOptions := metav1.ListOptions{}
if nodeName != "" {
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", nodeName)
}
if !withDependencies {
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets(namespace, podListOptions)
if err != nil {
return nil, err
}
for _, s := range statefulSets.Items {
applicationList = append(applicationList, models.K8sApplication{
UID: string(s.UID),
Name: s.Name,
Namespace: s.Namespace,
Kind: "StatefulSet",
Labels: s.Labels,
})
return kcl.convertPodsToApplications(pods, replicaSets, nil, nil, nil, nil)
}
pods, replicaSets, deployments, statefulSets, daemonSets, services, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
if err != nil {
return nil, err
}
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, "daemonset") {
daemonSets, err := kcl.cli.AppsV1().DaemonSets(namespace).List(context.TODO(), listOpts)
if err != nil {
return nil, err
}
return results, nil
}
for _, d := range daemonSets.Items {
applicationList = append(applicationList, models.K8sApplication{
UID: string(d.UID),
Name: d.Name,
Namespace: d.Namespace,
Kind: "DaemonSet",
Labels: d.Labels,
})
}
}
// 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{})
if kind == "" || strings.EqualFold(kind, "nakedpods") {
pods, _ := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})
for _, pod := range pods.Items {
naked := false
if len(pod.OwnerReferences) == 0 {
naked = true
} else {
managed := false
loop:
for _, ownerRef := range pod.OwnerReferences {
switch ownerRef.Kind {
case "Deployment", "DaemonSet", "ReplicaSet":
managed = true
break loop
}
}
if !managed {
naked = true
}
for _, pod := range pods {
if len(pod.OwnerReferences) > 0 {
ownerUID := string(pod.OwnerReferences[0].UID)
if _, exists := processedOwners[ownerUID]; exists {
continue
}
processedOwners[ownerUID] = struct{}{}
}
if naked {
applicationList = append(applicationList, models.K8sApplication{
UID: string(pod.UID),
Name: pod.Name,
Namespace: pod.Namespace,
Kind: "Pod",
Labels: pod.Labels,
})
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 _, 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 resource, nil
}
// 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)
}
}
}
return applicationList, nil
return applications, nil
}
// GetApplication gets a kubernetes workload (application) by kind and name. If Kind is not specified, gets the all
func (kcl *KubeClient) GetApplication(namespace, kind, name string) (models.K8sApplication, error) {
opts := metav1.GetOptions{}
switch strings.ToLower(kind) {
case "deployment":
d, err := kcl.cli.AppsV1().Deployments(namespace).Get(context.TODO(), name, opts)
if err != nil {
return models.K8sApplication{}, err
func (kcl *KubeClient) GetApplicationNamesFromSecret(secret models.K8sSecret, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]string, error) {
applications := []string{}
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 models.K8sApplication{
UID: string(d.UID),
Name: d.Name,
Namespace: d.Namespace,
Kind: "Deployment",
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{
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
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,
}
}
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 configurationOwners, nil
}
// GetApplicationConfigurationOwnersFromSecret gets a list of applications that use a specific Secret
// 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) {
configurationOwners := []models.K8sConfigurationOwnerResource{}
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
}
if application != nil {
configurationOwners = append(configurationOwners, models.K8sConfigurationOwnerResource{
Name: application.Name,
ResourceKind: application.Kind,
Id: application.UID,
})
}
}
}
}
return configurationOwners, nil
}

View File

@ -21,12 +21,11 @@ import (
)
const (
DefaultKubeClientQPS = 30
DefaultKubeClientBurst = 100
defaultKubeClientQPS = 30
defaultKubeClientBurst = 100
maxConcurrency = 30
)
const maxConcurrency = 30
type (
// ClientFactory is used to create Kubernetes clients
ClientFactory struct {
@ -34,17 +33,17 @@ type (
reverseTunnelService portainer.ReverseTunnelService
signatureService portainer.DigitalSignatureService
instanceID string
endpointClients map[string]*KubeClient
endpointProxyClients *cache.Cache
AddrHTTPS string
mu sync.Mutex
}
// KubeClient represent a service used to execute Kubernetes operations
KubeClient struct {
cli kubernetes.Interface
instanceID string
mu sync.Mutex
cli kubernetes.Interface
instanceID string
mu sync.Mutex
IsKubeAdmin bool
NonAdminNamespaces []string
}
)
@ -70,7 +69,6 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
signatureService: signatureService,
reverseTunnelService: reverseTunnelService,
instanceID: instanceID,
endpointClients: make(map[string]*KubeClient),
endpointProxyClients: cache.New(timeout, timeout),
AddrHTTPS: addrHTTPS,
}, nil
@ -80,42 +78,33 @@ func (factory *ClientFactory) GetInstanceID() (instanceID string) {
return factory.instanceID
}
// Remove the cached kube client so a new one can be created
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
factory.mu.Lock()
delete(factory.endpointClients, strconv.Itoa(int(endpointID)))
factory.mu.Unlock()
// Clear removes all cached kube clients
func (factory *ClientFactory) ClearClientCache() {
log.Debug().Msgf("kubernetes namespace permissions have changed, clearing the client cache")
factory.endpointProxyClients.Flush()
}
// GetKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
// If no client is registered, it will create a new client, register it, and returns it.
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
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()
// Remove the cached kube client so a new one can be created
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
factory.endpointProxyClients.Delete(strconv.Itoa(int(endpointID)))
}
// EE-6901: Do not lock
client, err := factory.createCachedAdminKubeClient(endpoint)
// GetPrivilegedKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
// 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 {
return nil, err
}
factory.mu.Lock()
defer factory.mu.Unlock()
// 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
factory.endpointProxyClients.Set(key, kcl, cache.DefaultExpiration)
return kcl, nil
}
// GetProxyKubeClient retrieves a KubeClient from the cache. You should be
@ -123,46 +112,47 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (*Kube
// kubernetes middleware.
func (factory *ClientFactory) GetProxyKubeClient(endpointID, userID string) (*KubeClient, bool) {
client, ok := factory.endpointProxyClients.Get(endpointID + "." + userID)
if !ok {
return nil, false
if ok {
return client.(*KubeClient), true
}
return client.(*KubeClient), true
return nil, false
}
// SetProxyKubeClient stores a kubeclient in the cache.
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
// 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)
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 {
return nil, err
return nil, fmt.Errorf("failed to get the complete client config from kubeconfig: %w", err)
}
cliConfig.QPS = DefaultKubeClientQPS
cliConfig.Burst = DefaultKubeClientBurst
clientConfig.QPS = defaultKubeClientQPS
clientConfig.Burst = defaultKubeClientBurst
cli, err := kubernetes.NewForConfig(cliConfig)
cli, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create a new clientset for the given config: %w", err)
}
return &KubeClient{
cli: cli,
instanceID: factory.instanceID,
cli: cli,
instanceID: factory.instanceID,
IsKubeAdmin: IsKubeAdmin,
NonAdminNamespaces: NonAdminNamespaces,
}, nil
}
func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
func (factory *ClientFactory) createCachedPrivilegedKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
cli, err := factory.CreateClient(endpoint)
if err != nil {
return nil, err
@ -235,8 +225,8 @@ func (factory *ClientFactory) buildAgentConfig(endpoint *portainer.Endpoint) (*r
}
config.Insecure = true
config.QPS = DefaultKubeClientQPS
config.Burst = DefaultKubeClientBurst
config.QPS = defaultKubeClientQPS
config.Burst = defaultKubeClientBurst
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return &agentHeaderRoundTripper{
@ -251,7 +241,7 @@ func (factory *ClientFactory) buildAgentConfig(endpoint *portainer.Endpoint) (*r
func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*rest.Config, error) {
tunnelAddr, err := factory.reverseTunnelService.TunnelAddr(endpoint)
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)
@ -266,8 +256,8 @@ func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*re
}
config.Insecure = true
config.QPS = DefaultKubeClientQPS
config.Burst = DefaultKubeClientBurst
config.QPS = defaultKubeClientQPS
config.Burst = defaultKubeClientBurst
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return &agentHeaderRoundTripper{
@ -294,8 +284,8 @@ func buildLocalConfig() (*rest.Config, error) {
return nil, err
}
config.QPS = DefaultKubeClientQPS
config.Burst = DefaultKubeClientBurst
config.QPS = defaultKubeClientQPS
config.Burst = defaultKubeClientBurst
return config, nil
}

View 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,
}
}

View 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,
}
}

View 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
}

View File

@ -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
}

View File

@ -151,12 +151,16 @@ func getApplicationsCount(ctx context.Context, kcl *KubeClient, namespace string
}
// + (naked pods)
nakedPods, err := kcl.GetApplications(namespace, "nakedpods")
if err != nil {
return 0, err
}
// TODO: Implement fetching of naked pods
// This is to be reworked as part of the dashboard refactor
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

View File

@ -2,183 +2,252 @@ package cli
import (
"context"
"fmt"
"strings"
models "github.com/portainer/portainer/api/http/models/kubernetes"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log"
netv1 "k8s.io/api/networking/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) {
var controllers []models.K8sIngressController
// We know that each existing class points to a controller so we can start
// by collecting these easy ones.
classClient := kcl.cli.NetworkingV1().IngressClasses()
classList, err := classClient.List(context.Background(), metav1.ListOptions{})
classeses, err := kcl.cli.NetworkingV1().IngressClasses().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
// We want to know which of these controllers is in use.
var ingresses []models.K8sIngressInfo
namespaces, err := kcl.GetNamespaces()
ingresses, err := kcl.GetIngresses("")
if err != nil {
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{})
for _, ingress := range ingresses {
usedClasses[ingress.ClassName] = struct{}{}
}
for _, class := range classList.Items {
var controller models.K8sIngressController
controller.Name = class.Spec.Controller
controller.ClassName = class.Name
// If the class is used mark it as such.
results := []models.K8sIngressController{}
for _, class := range classeses.Items {
ingressClass := parseIngressClass(class)
if _, ok := usedClasses[class.Name]; ok {
controller.Used = true
ingressClass.Used = true
}
switch {
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)
results = append(results, ingressClass)
}
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
}
// 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.
func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, error) {
// Fetch ingress classes to build a map. We will later use the map to lookup
// each ingresses "type".
classes := make(map[string]string)
classClient := kcl.cli.NetworkingV1().IngressClasses()
classList, err := classClient.List(context.Background(), metav1.ListOptions{})
if kcl.IsKubeAdmin {
return kcl.fetchIngresses(namespace)
}
return kcl.fetchIngressesForNonAdmin(namespace)
}
// 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 {
return nil, err
}
for _, class := range classList.Items {
// Write the ingress classes "type" to our map.
classes[class.Name] = class.Spec.Controller
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
results := make([]models.K8sIngressInfo, 0)
for _, ingress := range ingresses {
if _, ok := nonAdminNamespaceSet[ingress.Namespace]; ok {
results = append(results, ingress)
}
}
// Fetch each ingress.
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
ingressList, err := ingressClient.List(context.Background(), metav1.ListOptions{})
return results, nil
}
// 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 {
return nil, err
}
var infos []models.K8sIngressInfo
for _, ingress := range ingressList.Items {
var info models.K8sIngressInfo
info.Name = ingress.Name
info.UID = string(ingress.UID)
info.Namespace = namespace
ingressClass := ingress.Spec.IngressClassName
info.ClassName = ""
if ingressClass != nil {
info.ClassName = *ingressClass
}
info.Type = classes[info.ClassName]
info.Annotations = ingress.Annotations
info.Labels = ingress.Labels
info.CreationDate = ingress.CreationTimestamp.Time
// Gather TLS information.
for _, v := range ingress.Spec.TLS {
var tls models.K8sIngressTLS
tls.Hosts = v.Hosts
tls.SecretName = v.SecretName
info.TLS = append(info.TLS, tls)
}
// Gather list of paths and hosts.
hosts := make(map[string]struct{})
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{}{}
if r.HTTP == nil {
continue
}
// There are multiple paths per rule. We want to flatten the list
// for our frontend.
for _, p := range r.HTTP.Paths {
var path models.K8sIngressPath
path.IngressName = info.Name
path.Host = r.Host
path.Path = p.Path
if p.PathType != nil {
path.PathType = string(*p.PathType)
}
path.ServiceName = p.Backend.Service.Name
path.Port = int(p.Backend.Service.Port.Number)
info.Paths = append(info.Paths, path)
}
}
// Store list of hosts.
for host := range hosts {
info.Hosts = append(info.Hosts, host)
}
infos = append(infos, info)
ingressClasses, err := kcl.fetchIngressClasses()
if err != nil {
return nil, err
}
return infos, nil
results := []models.K8sIngressInfo{}
if len(ingresses.Items) == 0 {
return results, nil
}
for _, ingress := range ingresses.Items {
result := parseIngress(ingress)
if ingress.Spec.IngressClassName != nil {
result.Type = findUsedIngressFromIngressClasses(ingressClasses, *ingress.Spec.IngressClassName).Name
}
results = append(results, result)
}
return results, nil
}
// 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,
})
}
hosts := make(map[string]struct{})
for _, r := range ingress.Spec.Rules {
hosts[r.Host] = struct{}{}
if r.HTTP == nil {
continue
}
for _, p := range r.HTTP.Paths {
var path models.K8sIngressPath
path.IngressName = result.Name
path.Host = r.Host
path.Path = p.Path
if p.PathType != nil {
path.PathType = string(*p.PathType)
}
path.ServiceName = p.Backend.Service.Name
path.Port = int(p.Backend.Service.Port.Number)
result.Paths = append(result.Paths, path)
}
}
for host := range hosts {
result.Hosts = append(result.Hosts, host)
}
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 models.K8sIngressController{}
}
// 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 {
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
var ingress netv1.Ingress
ingress.Name = info.Name
ingress.Namespace = info.Namespace
if info.ClassName != "" {
ingress.Spec.IngressClassName = &info.ClassName
ingress := kcl.convertToK8sIngress(info, owner)
_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Create(context.Background(), &ingress, metav1.CreateOptions{})
if err != nil {
return err
}
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.
var tls []netv1.IngressTLS
for _, i := range info.TLS {
return nil
}
// 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{
Hosts: i.Hosts,
SecretName: i.SecretName,
Hosts: t.Hosts,
SecretName: t.SecretName,
})
}
ingress.Spec.TLS = tls
result.Spec.TLS = tls
// Parse "paths" into rules with paths.
rules := make(map[string][]netv1.HTTPIngressPath)
for _, path := range info.Paths {
pathType := netv1.PathType(path.PathType)
@ -197,7 +266,7 @@ func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInf
}
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,
IngressRuleValue: netv1.IngressRuleValue{
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 {
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,
})
}
}
_, err := ingressClient.Create(context.Background(), &ingress, metav1.CreateOptions{})
return err
return result
}
// DeleteIngresses processes a K8sIngressDeleteRequest by deleting each ingress
// in its given namespace.
func (kcl *KubeClient) DeleteIngresses(reqs models.K8sIngressDeleteRequests) error {
var err error
for namespace := range reqs {
for _, ingress := range reqs[namespace] {
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
err = ingressClient.Delete(
err := kcl.cli.NetworkingV1().Ingresses(namespace).Delete(
context.Background(),
ingress,
metav1.DeleteOptions{},
)
if err != nil {
return err
}
}
}
return err
return nil
}
// UpdateIngress updates an existing ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInfo) error {
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
ingress, err := ingressClient.Get(context.Background(), info.Name, metav1.GetOptions{})
ingress := kcl.convertToK8sIngress(info, "")
_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Update(context.Background(), &ingress, metav1.UpdateOptions{})
if err != nil {
return err
}
ingress.Name = info.Name
ingress.Namespace = info.Namespace
if info.ClassName != "" {
ingress.Spec.IngressClassName = &info.ClassName
}
ingress.Annotations = info.Annotations
return nil
}
// Store TLS information.
var tls []netv1.IngressTLS
for _, i := range info.TLS {
tls = append(tls, netv1.IngressTLS{
Hosts: i.Hosts,
SecretName: i.SecretName,
})
}
ingress.Spec.TLS = tls
// Parse "paths" into rules with paths.
rules := make(map[string][]netv1.HTTPIngressPath)
for _, path := range info.Paths {
pathType := netv1.PathType(path.PathType)
rules[path.Host] = append(rules[path.Host], netv1.HTTPIngressPath{
Path: path.Path,
PathType: &pathType,
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: path.ServiceName,
Port: netv1.ServiceBackendPort{
Number: int32(path.Port),
},
},
},
})
// 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.
func (kcl *KubeClient) CombineIngressWithService(ingress models.K8sIngressInfo) (models.K8sIngressInfo, error) {
services, err := kcl.GetServices(ingress.Namespace)
if err != nil {
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)
}
ingress.Spec.Rules = make([]netv1.IngressRule, 0)
for rule, paths := range rules {
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
Host: rule,
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: paths,
},
},
})
}
// 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,
})
serviceMap := kcl.buildServicesMap(services)
for pathIndex, path := range ingress.Paths {
if _, ok := serviceMap[path.ServiceName]; ok {
ingress.Paths[pathIndex].HasService = true
}
}
_, err = ingressClient.Update(context.Background(), ingress, metav1.UpdateOptions{})
return err
return ingress, nil
}
// CombineIngressesWithServices combines a list of ingresses with a list of services that are being used by the ingresses.
// this is required to display the services that are being used by the ingresses in the UI list view.
func (kcl *KubeClient) CombineIngressesWithServices(ingresses []models.K8sIngressInfo) ([]models.K8sIngressInfo, error) {
services, err := kcl.GetServices("")
if err != nil {
if k8serrors.IsUnauthorized(err) {
return nil, fmt.Errorf("an error occurred during the CombineIngressesWithServices operation, unauthorized access to the Kubernetes API. Error: %w", err)
}
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 {
for pathIndex, path := range ingress.Paths {
if _, ok := serviceMap[path.ServiceName]; ok {
(ingresses)[ingressIndex].Paths[pathIndex].HasService = true
}
}
}
return ingresses, nil
}

View File

@ -3,20 +3,27 @@ package cli
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
models "github.com/portainer/portainer/api/http/models/kubernetes"
"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"
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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
systemNamespaceLabel = "io.portainer.kubernetes.namespace.system"
namespaceOwnerLabel = "io.portainer.kubernetes.resourcepool.owner"
namespaceNameLabel = "io.portainer.kubernetes.resourcepool.name"
)
func defaultSystemNamespaces() map[string]struct{} {
@ -29,24 +36,69 @@ func defaultSystemNamespaces() map[string]struct{} {
}
// 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) {
namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
if kcl.IsKubeAdmin {
return kcl.fetchNamespaces()
}
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 {
results[ns.Name] = portainer.K8sNamespaceInfo{
IsSystem: isSystemNamespace(ns),
IsDefault: ns.Name == defaultNamespace,
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
results := make(map[string]portainer.K8sNamespaceInfo)
for _, namespace := range namespaces {
if _, exists := nonAdminNamespaceSet[namespace.Name]; exists {
results[namespace.Name] = namespace
}
}
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).
func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, error) {
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
}
result := portainer.K8sNamespaceInfo{
IsSystem: isSystemNamespace(*namespace),
IsDefault: namespace.Name == defaultNamespace,
}
return result, nil
return parseNamespace(namespace), nil
}
// 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{
"io.portainer.kubernetes.resourcepool.name": stackutils.SanitizeLabel(info.Name),
"io.portainer.kubernetes.resourcepool.owner": stackutils.SanitizeLabel(info.Owner),
namespaceNameLabel: stackutils.SanitizeLabel(info.Name),
namespaceOwnerLabel: stackutils.SanitizeLabel(info.Owner),
}
var ns v1.Namespace
var ns corev1.Namespace
ns.Name = info.Name
ns.Annotations = info.Annotations
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 {
log.Error().
Err(err).
Str("Namespace", info.Name).
Msg("Failed to create the namespace")
return err
return nil, err
}
if info.ResourceQuota != nil && info.ResourceQuota.Enabled {
log.Info().Msgf("Creating resource quota for namespace %s", info.Name)
log.Debug().Msgf("Creating resource quota with details: %+v", info.ResourceQuota)
resourceQuota := &v1.ResourceQuota{
resourceQuota := &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: "portainer-rq-" + info.Name,
Namespace: info.Name,
Labels: portainerLabels,
},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{},
Spec: corev1.ResourceQuotaSpec{
Hard: corev1.ResourceList{},
},
}
@ -103,28 +150,28 @@ func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error {
cpu := resource.MustParse(info.ResourceQuota.CPU)
if memory.Value() > 0 {
memQuota := memory
resourceQuota.Spec.Hard[v1.ResourceLimitsMemory] = memQuota
resourceQuota.Spec.Hard[v1.ResourceRequestsMemory] = memQuota
resourceQuota.Spec.Hard[corev1.ResourceLimitsMemory] = memQuota
resourceQuota.Spec.Hard[corev1.ResourceRequestsMemory] = memQuota
}
if cpu.Value() > 0 {
cpuQuota := cpu
resourceQuota.Spec.Hard[v1.ResourceLimitsCPU] = cpuQuota
resourceQuota.Spec.Hard[v1.ResourceRequestsCPU] = cpuQuota
resourceQuota.Spec.Hard[corev1.ResourceLimitsCPU] = cpuQuota
resourceQuota.Spec.Hard[corev1.ResourceRequestsCPU] = cpuQuota
}
}
_, err := kcl.cli.CoreV1().ResourceQuotas(info.Name).Create(context.Background(), resourceQuota, metav1.CreateOptions{})
if err != nil {
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]
if hasSystemLabel {
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.
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceDetails) error {
client := kcl.cli.CoreV1().Namespaces()
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error) {
namespace := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: info.Name,
Annotations: info.Annotations,
},
}
var ns v1.Namespace
ns.Name = info.Name
ns.Annotations = info.Annotations
_, err := client.Update(context.Background(), &ns, metav1.UpdateOptions{})
return err
return kcl.cli.CoreV1().Namespaces().Update(context.Background(), &namespace, metav1.UpdateOptions{})
}
func (kcl *KubeClient) DeleteNamespace(namespace string) error {
client := kcl.cli.CoreV1().Namespaces()
namespaces, err := client.List(context.Background(), metav1.ListOptions{})
func (kcl *KubeClient) DeleteNamespace(namespaceName string) (*corev1.Namespace, error) {
namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.Background(), namespaceName, metav1.GetOptions{})
if err != nil {
return err
return nil, err
}
for _, ns := range namespaces.Items {
if ns.Name == namespace {
return client.Delete(
context.Background(),
namespace,
metav1.DeleteOptions{},
)
}
err = kcl.cli.CoreV1().Namespaces().Delete(context.Background(), namespaceName, metav1.DeleteOptions{})
if err != nil {
return nil, err
}
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
}

View File

@ -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) {
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{
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}, config),
instanceID: "instance",
}

View File

@ -2,6 +2,7 @@ package cli
import (
"context"
"fmt"
"strconv"
"time"
@ -9,10 +10,69 @@ import (
"github.com/pkg/errors"
"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"
)
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.
// 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
@ -24,7 +84,7 @@ func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountNam
podPrefix := userShellPodPrefix(serviceAccountName)
podSpec := &v1.Pod{
podSpec := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: podPrefix,
Namespace: portainerNamespace,
@ -32,20 +92,20 @@ func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountNam
"kubernetes.io/pod.type": "kubectl-shell",
},
},
Spec: v1.PodSpec{
Spec: corev1.PodSpec{
TerminationGracePeriodSeconds: new(int64),
ServiceAccountName: serviceAccountName,
Containers: []v1.Container{
Containers: []corev1.Container{
{
Name: "kubectl-shell-container",
Image: shellPodImage,
Command: []string{"sleep"},
// Specify sleep time to prevent zombie pods in case portainer process is terminated
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)
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{})
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.
// 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")
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
}

View File

@ -2,198 +2,17 @@ package cli
import (
"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"
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 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
// 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.
// otherwise, RBAC is not enabled.
func (kcl *KubeClient) IsRBACEnabled() (bool, error) {
namespace := "default"
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)
_, err := kcl.cli.RbacV1().ClusterRoles().List(context.TODO(), metav1.ListOptions{})
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
}
// 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
return true, nil
}

View 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
}

View File

@ -3,11 +3,66 @@ package cli
import (
"context"
models "github.com/portainer/portainer/api/http/models/kubernetes"
rbacv1 "k8s.io/api/rbac/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
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 {
return []rbacv1.PolicyRule{
{

View 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,
}
}

View File

@ -3,17 +3,182 @@ package cli
import (
"context"
"errors"
"fmt"
"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"
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 {
serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName, kcl.instanceID)
serviceAccountSecret := &v1.Secret{
serviceAccountSecret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: serviceAccountSecretName,

View File

@ -2,99 +2,135 @@ package cli
import (
"context"
"fmt"
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"
labels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/intstr"
)
// GetServices gets all the services for a given namespace in a k8s endpoint.
func (kcl *KubeClient) GetServices(namespace string, lookupApplications bool) ([]models.K8sServiceInfo, error) {
client := kcl.cli.CoreV1().Services(namespace)
// GetServices gets all the services for either at the cluster level or a given namespace in a k8s endpoint.
// It returns a list of K8sServiceInfo objects.
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 {
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 {
servicePorts := make([]models.K8sServicePort, 0)
for _, port := range service.Spec.Ports {
servicePorts = append(servicePorts, models.K8sServicePort{
Name: port.Name,
NodePort: int(port.NodePort),
Port: int(port.Port),
Protocol: string(port.Protocol),
TargetPort: port.TargetPort.String(),
})
}
results = append(results, parseService(service))
}
ingressStatus := make([]models.K8sServiceIngress, 0)
for _, status := range service.Status.LoadBalancer.Ingress {
ingressStatus = append(ingressStatus, models.K8sServiceIngress{
IP: status.IP,
Host: status.Hostname,
})
}
return results, nil
}
var applications []models.K8sApplication
if lookupApplications {
applications, _ = kcl.getOwningApplication(namespace, service.Spec.Selector)
}
result = append(result, models.K8sServiceInfo{
Name: service.Name,
UID: string(service.GetUID()),
Type: string(service.Spec.Type),
Namespace: service.Namespace,
CreationTimestamp: service.GetCreationTimestamp().String(),
AllocateLoadBalancerNodePorts: service.Spec.AllocateLoadBalancerNodePorts,
Ports: servicePorts,
IngressStatus: ingressStatus,
Labels: service.GetLabels(),
Annotations: service.GetAnnotations(),
ClusterIPs: service.Spec.ClusterIPs,
ExternalName: service.Spec.ExternalName,
ExternalIPs: service.Spec.ExternalIPs,
Applications: applications,
// 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)
for _, port := range service.Spec.Ports {
servicePorts = append(servicePorts, models.K8sServicePort{
Name: port.Name,
NodePort: int(port.NodePort),
Port: int(port.Port),
Protocol: string(port.Protocol),
TargetPort: port.TargetPort.String(),
})
}
return result, nil
ingressStatus := make([]models.K8sServiceIngress, 0)
for _, status := range service.Status.LoadBalancer.Ingress {
ingressStatus = append(ingressStatus, models.K8sServiceIngress{
IP: status.IP,
Host: status.Hostname,
})
}
return models.K8sServiceInfo{
Name: service.Name,
UID: string(service.GetUID()),
Type: string(service.Spec.Type),
Namespace: service.Namespace,
CreationDate: service.GetCreationTimestamp().String(),
AllocateLoadBalancerNodePorts: service.Spec.AllocateLoadBalancerNodePorts,
Ports: servicePorts,
IngressStatus: ingressStatus,
Labels: service.GetLabels(),
Annotations: service.GetAnnotations(),
ClusterIPs: service.Spec.ClusterIPs,
ExternalName: service.Spec.ExternalName,
ExternalIPs: service.Spec.ExternalIPs,
Selector: service.Spec.Selector,
}
}
func (kcl *KubeClient) fillService(info models.K8sServiceInfo) v1.Service {
var service v1.Service
// convertToK8sService converts a K8sServiceInfo object back to a k8s native service object.
// 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.Spec.Type = v1.ServiceType(info.Type)
service.Spec.Type = corev1.ServiceType(info.Type)
service.Namespace = info.Namespace
service.Annotations = info.Annotations
service.Labels = info.Labels
service.Spec.AllocateLoadBalancerNodePorts = info.AllocateLoadBalancerNodePorts
service.Spec.Selector = info.Selector
// Set ports.
for _, p := range info.Ports {
var port v1.ServicePort
port := corev1.ServicePort{}
port.Name = p.Name
port.NodePort = int32(p.NodePort)
port.Port = int32(p.Port)
port.Protocol = v1.Protocol(p.Protocol)
port.Protocol = corev1.Protocol(p.Protocol)
port.TargetPort = intstr.FromString(p.TargetPort)
service.Spec.Ports = append(service.Spec.Ports, port)
}
// Set ingresses.
for _, i := range info.IngressStatus {
service.Status.LoadBalancer.Ingress = append(
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.
func (kcl *KubeClient) CreateService(namespace string, info models.K8sServiceInfo) error {
serviceClient := kcl.cli.CoreV1().Services(namespace)
service := kcl.fillService(info)
_, err := serviceClient.Create(context.Background(), &service, metav1.CreateOptions{})
service := kcl.convertToK8sService(info)
_, err := kcl.cli.CoreV1().Services(namespace).Create(context.Background(), &service, metav1.CreateOptions{})
return err
}
// DeleteServices processes a K8sServiceDeleteRequest by deleting each service
// in its given namespace.
func (kcl *KubeClient) DeleteServices(reqs models.K8sServiceDeleteRequests) error {
var err error
for namespace := range reqs {
for _, service := range reqs[namespace] {
serviceClient := kcl.cli.CoreV1().Services(namespace)
err = serviceClient.Delete(
context.Background(),
service,
metav1.DeleteOptions{},
)
err := kcl.cli.CoreV1().Services(namespace).Delete(context.Background(), service, metav1.DeleteOptions{})
if err != nil {
return err
}
}
}
return err
return nil
}
// UpdateService updates service in a given namespace in a k8s endpoint.
func (kcl *KubeClient) UpdateService(namespace string, info models.K8sServiceInfo) error {
serviceClient := kcl.cli.CoreV1().Services(namespace)
service := kcl.fillService(info)
_, err := serviceClient.Update(context.Background(), &service, metav1.UpdateOptions{})
service := kcl.convertToK8sService(info)
_, err := kcl.cli.CoreV1().Services(namespace).Update(context.Background(), &service, metav1.UpdateOptions{})
return err
}
// getOwningApplication gets the application that owns the given service selector.
func (kcl *KubeClient) getOwningApplication(namespace string, selector map[string]string) ([]models.K8sApplication, error) {
if len(selector) == 0 {
return nil, nil
}
selectorLabels := labels.SelectorFromSet(selector).String()
// look for replicasets first, limit 1 (we only support one owner)
replicasets, err := kcl.cli.AppsV1().ReplicaSets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selectorLabels, Limit: 1})
if err != nil {
return nil, err
}
var meta metav1.Object
if replicasets != nil && len(replicasets.Items) > 0 {
meta = replicasets.Items[0].GetObjectMeta()
} else {
// 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})
// CombineServicesWithApplications retrieves applications based on service selectors in a given namespace
// for all services, it lists pods based on the service selector and converts the pod to an application
// if replicasets are found, it updates the owner reference to deployment
// 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) {
if containsServiceWithSelector(services) {
updatedServices := make([]models.K8sServiceInfo, len(services))
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
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)
}
if pods == nil || len(pods.Items) == 0 {
return nil, nil
for index, service := range services {
updatedService := service
application, err := kcl.GetApplicationFromServiceSelector(pods, service, replicaSets)
if err != nil {
return services, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to get application from service. Error: %w", err)
}
if application != nil {
updatedService.Applications = append(updatedService.Applications, *application)
}
updatedServices[index] = updatedService
}
meta = pods.Items[0].GetObjectMeta()
return updatedServices, nil
}
return makeApplication(meta), nil
return services, nil
}
func makeApplication(meta metav1.Object) []models.K8sApplication {
ownerReferences := meta.GetOwnerReferences()
if len(ownerReferences) == 0 {
return nil
}
// 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,
},
// containsServiceWithSelector checks if a list of services contains a service with a selector
// it returns true if any service has a selector, otherwise false
func containsServiceWithSelector(services []models.K8sServiceInfo) bool {
for _, service := range services {
if len(service.Selector) > 0 {
return true
}
}
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
}

View File

@ -4,23 +4,76 @@ import (
"context"
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"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// GetServiceAccount returns the portainer ServiceAccountName associated to the specified user.
func (kcl *KubeClient) GetServiceAccount(tokenData *portainer.TokenData) (*v1.ServiceAccount, error) {
var portainerServiceAccountName string
// GetServiceAccounts gets all the service accounts for either at the cluster level or a given namespace in a k8s endpoint.
// It returns a list of K8sServiceAccount objects.
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 {
portainerServiceAccountName = portainerClusterAdminServiceAccountName
} else {
portainerServiceAccountName = UserServiceAccountName(int(tokenData.ID), kcl.instanceID)
portainerUserServiceAccountName = portainerClusterAdminServiceAccountName
}
// 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 {
return nil, err
}
@ -69,7 +122,7 @@ func (kcl *KubeClient) ensureRequiredResourcesExist() error {
}
func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error {
serviceAccount := &v1.ServiceAccount{
serviceAccount := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: serviceAccountName,
},

View File

@ -18,9 +18,9 @@ func Test_GetServiceAccount(t *testing.T) {
instanceID: "test",
}
tokenData := &portainer.TokenData{ID: 1}
_, err := k.GetServiceAccount(tokenData)
_, err := k.GetPortainerUserServiceAccount(tokenData)
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{})
sa, err := k.GetServiceAccount(tokenData)
sa, err := k.GetPortainerUserServiceAccount(tokenData)
if err != nil {
t.Errorf("GetServiceAccount should succeed; err=%s", err)
t.Errorf("GetPortainerUserServiceAccount should succeed; err=%s", err)
}
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{})
sa, err := k.GetServiceAccount(tokenData)
sa, err := k.GetPortainerUserServiceAccount(tokenData)
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"
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)
}
})

View 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
}

View File

@ -57,7 +57,7 @@ func (h *HandlerDeleteK8sRegistrySecrets) Execute(pa portainer.PendingAction, en
return err
}
kubeClient, err := h.kubeFactory.GetKubeClient(endpoint)
kubeClient, err := h.kubeFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return err
}

View File

@ -80,7 +80,7 @@ func (service *PendingActionsService) execute(environmentID portainer.EndpointID
} else {
// For Kubernetes endpoints, we need to check if the endpoint is up by
// creating a kube client and performing a simple operation
client, err := service.kubeFactory.GetKubeClient(endpoint)
client, err := service.kubeFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
log.Debug().Msgf("failed to create Kubernetes client for environment %d: %v", environmentID, err)
return

View File

@ -14,7 +14,7 @@ import (
"github.com/portainer/portainer/pkg/featureflags"
"golang.org/x/oauth2"
v1 "k8s.io/api/core/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/version"
)
@ -595,8 +595,14 @@ type (
JobType int
K8sNamespaceInfo struct {
IsSystem bool `json:"IsSystem"`
IsDefault bool `json:"IsDefault"`
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"`
IsDefault bool `json:"IsDefault"`
ResourceQuota *corev1.ResourceQuota `json:"ResourceQuota"`
}
K8sNodeLimits struct {
@ -1491,22 +1497,22 @@ type (
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
IsRBACEnabled() (bool, error)
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
GetPortainerUserServiceAccount(tokendata *TokenData) (*corev1.ServiceAccount, error)
GetServiceAccountBearerToken(userID int) (string, 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)
HasStackName(namespace string, stackName string) (bool, error)
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
CreateNamespace(info models.K8sNamespaceDetails) error
UpdateNamespace(info models.K8sNamespaceDetails) error
CreateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error)
UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error)
GetNamespaces() (map[string]K8sNamespaceInfo, error)
GetNamespace(string) (K8sNamespaceInfo, error)
DeleteNamespace(namespace string) error
GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error)
DeleteNamespace(namespace string) (*corev1.Namespace, error)
GetConfigMaps(namespace string) ([]models.K8sConfigMap, error)
GetSecrets(namespace string) ([]models.K8sSecret, error)
GetIngressControllers() (models.K8sIngressControllers, error)
GetApplications(namespace, kind string) ([]models.K8sApplication, error)
GetApplication(namespace, kind, name string) (models.K8sApplication, error)
GetApplications(namespace, nodename string, withDependencies bool) ([]models.K8sApplication, error)
GetMetrics() (models.K8sMetrics, error)
GetStorage() ([]KubernetesStorageClassConfig, error)
CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error
@ -1515,7 +1521,7 @@ type (
DeleteIngresses(reqs models.K8sIngressDeleteRequests) error
CreateService(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
GetNodesLimits() (K8sNodesLimits, error)
GetMaxResourceLimits(name string, overCommitEnabled bool, resourceOverCommitPercent int) (K8sNodeLimits, error)

View File

@ -1,5 +1,4 @@
import { EnvironmentStatus } from '@/react/portainer/environments/types';
import { getSelfSubjectAccessReview } from '@/react/kubernetes/namespaces/getSelfSubjectAccessReview';
import { updateAxiosAdapter } from '@/portainer/services/axios';
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
@ -96,13 +95,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
}
try {
const status = await checkEndpointStatus(
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')
);
const status = await checkEndpointStatus(endpoint);
if (endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
await updateEndpointStatus(endpoint, status);
@ -131,9 +124,9 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
return false;
}
async function checkEndpointStatus(promise) {
async function checkEndpointStatus(endpoint) {
try {
await promise;
await KubernetesHealthService.ping(endpoint.Id);
return EnvironmentStatus.Up;
} catch (e) {
return EnvironmentStatus.Down;
@ -459,10 +452,10 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
const resourcePools = {
name: 'kubernetes.resourcePools',
url: '/pools',
url: '/namespaces',
views: {
'content@': {
component: 'kubernetesResourcePoolsView',
component: 'kubernetesNamespacesView',
},
},
data: {
@ -511,7 +504,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
const volumes = {
name: 'kubernetes.volumes',
url: '/volumes',
url: '/volumes?tab',
views: {
'content@': {
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(helmApplication);
$stateRegistryProvider.register(applications);
@ -621,5 +659,10 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
$stateRegistryProvider.register(ingresses);
$stateRegistryProvider.register(ingressesCreate);
$stateRegistryProvider.register(ingressesEdit);
$stateRegistryProvider.register(moreResources);
$stateRegistryProvider.register(serviceAccounts);
$stateRegistryProvider.register(clusterRoles);
$stateRegistryProvider.register(roles);
},
]);

View File

@ -77,7 +77,7 @@ class KubernetesConfigMapConverter {
static createPayload(data) {
const res = new KubernetesConfigMapCreatePayload();
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: '' });
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
@ -115,7 +115,7 @@ class KubernetesConfigMapConverter {
const res = new KubernetesConfigMap();
res.Id = formValues.Id;
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.Namespace = formValues.ResourcePool;
res.ConfigurationOwner = formValues.ConfigurationOwner;
res.Data = formValues.Data;
return res;

View File

@ -9,7 +9,7 @@ class KubernetesSecretConverter {
static createPayload(secret) {
const res = new KubernetesSecretCreatePayload();
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
res.metadata.namespace = secret.Namespace.Namespace.Name;
res.type = secret.Type;
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
@ -100,7 +100,7 @@ class KubernetesSecretConverter {
static configurationFormValuesToSecret(formValues) {
const res = new KubernetesApplicationSecret();
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.Namespace = formValues.ResourcePool;
res.Type = formValues.Type;
res.ConfigurationOwner = formValues.ConfigurationOwner;
res.Data = formValues.Data;

View File

@ -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;

View File

@ -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);
});
});

View File

@ -41,9 +41,11 @@ export class Application {
annotations?: Record<string, string>;
};
Limits: {
Cpu?: number;
Memory?: number;
Resource?: {
cpuLimit?: number;
cpuRequest?: number;
memoryLimit?: number;
memoryRequest?: number;
};
ServiceType?: ServiceType;
@ -106,7 +108,7 @@ export class Application {
this.Pods = [];
this.Containers = [];
this.Metadata = {};
this.Limits = {};
this.Resource = {};
this.ServiceId = '';
this.ServiceName = '';
this.HeadlessServiceName = undefined;

View File

@ -3,5 +3,7 @@ export class StorageClass {
Provisioner: string = '';
ReclaimPolicy: string = '';
AllowVolumeExpansion: boolean = false;
}

View File

@ -6,7 +6,7 @@ import { PersistentVolumeClaim } from './PersistentVolumeClaim';
type VolumeResourcePool = ReturnType<typeof KubernetesResourcePool>;
export class Volume {
ResourcePool: VolumeResourcePool = {} as VolumeResourcePool;
ResourcePool?: VolumeResourcePool = {} as VolumeResourcePool;
PersistentVolumeClaim: PersistentVolumeClaim = {} as PersistentVolumeClaim;

View File

@ -11,8 +11,6 @@ export const applicationsModule = angular
.component(
'kubernetesApplicationsDatatable',
r2a(withUIRouter(withCurrentUser(ApplicationsDatatable)), [
'dataset',
'isLoading',
'namespace',
'namespaces',
'onNamespaceChange',

View File

@ -11,11 +11,7 @@ export const clusterManagementModule = angular
.module('portainer.kubernetes.react.components.clusterManagement', [])
.component(
'kubernetesNodeApplicationsDatatable',
r2a(withUIRouter(withCurrentUser(NodeApplicationsDatatable)), [
'dataset',
'isLoading',
'onRefresh',
])
r2a(withUIRouter(withCurrentUser(NodeApplicationsDatatable)), [])
)
.component(
'resourceEventsDatatable',

View File

@ -62,7 +62,6 @@ import { kubeEnvVarValidationSchema } from '@/react/kubernetes/applications/comp
import { IntegratedAppsDatatable } from '@/react/kubernetes/components/IntegratedAppsDatatable/IntegratedAppsDatatable';
import { applicationsModule } from './applications';
import { volumesModule } from './volumes';
import { namespacesModule } from './namespaces';
import { clusterManagementModule } from './clusterManagement';
import { registriesModule } from './registries';
@ -70,7 +69,6 @@ import { registriesModule } from './registries';
export const ngModule = angular
.module('portainer.kubernetes.react.components', [
applicationsModule,
volumesModule,
namespacesModule,
clusterManagementModule,
registriesModule,
@ -213,13 +211,10 @@ export const ngModule = angular
.component(
'kubernetesApplicationsStacksDatatable',
r2a(withUIRouter(withCurrentUser(ApplicationsStacksDatatable)), [
'dataset',
'onRefresh',
'onRemove',
'namespace',
'namespaces',
'onNamespaceChange',
'isLoading',
'showSystem',
'setSystemResources',
])

View File

@ -12,11 +12,7 @@ export const namespacesModule = angular
.module('portainer.kubernetes.react.components.namespaces', [])
.component(
'kubernetesNamespacesDatatable',
r2a(withUIRouter(withCurrentUser(NamespacesDatatable)), [
'dataset',
'onRemove',
'onRefresh',
])
r2a(withUIRouter(withCurrentUser(NamespacesDatatable)), [])
)
.component(
'kubernetesNamespaceApplicationsDatatable',

View File

@ -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;

View File

@ -13,6 +13,11 @@ import { ConfigmapsAndSecretsView } from '@/react/kubernetes/configs/ListView/Co
import { CreateNamespaceView } from '@/react/kubernetes/namespaces/CreateView/CreateNamespaceView';
import { ApplicationDetailsView } from '@/react/kubernetes/applications/DetailsView/ApplicationDetailsView';
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
.module('portainer.kubernetes.react.views', [])
@ -20,10 +25,18 @@ export const viewsModule = angular
'kubernetesCreateNamespaceView',
r2a(withUIRouter(withReactQuery(withCurrentUser(CreateNamespaceView))), [])
)
.component(
'kubernetesNamespacesView',
r2a(withUIRouter(withReactQuery(withCurrentUser(NamespacesView))), [])
)
.component(
'kubernetesServicesView',
r2a(withUIRouter(withReactQuery(withCurrentUser(ServicesView))), [])
)
.component(
'kubernetesVolumesView',
r2a(withUIRouter(withReactQuery(withCurrentUser(VolumesView))), [])
)
.component(
'kubernetesIngressesView',
r2a(
@ -60,4 +73,16 @@ export const viewsModule = angular
.component(
'kubernetesConsoleView',
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;

View File

@ -14,7 +14,6 @@ import { notifyError } from '@/portainer/services/notifications';
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
import { generateNewIngressesFromFormPaths } from '@/react/kubernetes/applications/CreateView/application-services/utils';
import { KubernetesPod } from '../pod/models';
import { KubernetesApplication } from '../models/application/models';
class KubernetesApplicationService {
/* #region CONSTRUCTOR */
@ -64,7 +63,7 @@ class KubernetesApplicationService {
apiService = this.KubernetesDaemonSetService;
} else if (app.ApplicationType === KubernetesApplicationTypes.StatefulSet) {
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;
} else {
throw new PortainerError('Unable to determine which association to use to retrieve API Service');

View File

@ -2,17 +2,17 @@ import angular from 'angular';
import PortainerError from 'Portainer/error';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import KubernetesNamespaceConverter from 'Kubernetes/converters/namespace';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { updateNamespaces } from 'Kubernetes/store/namespace';
import $allSettled from 'Portainer/services/allSettled';
import { getSelfSubjectAccessReview } from '@/react/kubernetes/namespaces/getSelfSubjectAccessReview';
class KubernetesNamespaceService {
/* @ngInject */
constructor($async, KubernetesNamespaces, LocalStorage, $state) {
constructor($async, KubernetesNamespaces, Authentication, LocalStorage, $state) {
this.$async = $async;
this.$state = $state;
this.KubernetesNamespaces = KubernetesNamespaces;
this.LocalStorage = LocalStorage;
this.Authentication = Authentication;
this.getAsync = this.getAsync.bind(this);
this.getAllAsync = this.getAllAsync.bind(this);
@ -68,17 +68,13 @@ class KubernetesNamespaceService {
try {
// get the list of all namespaces (RBAC allows users to see the list of namespaces)
const data = await this.KubernetesNamespaces().get().$promise;
// get the status of each namespace with accessReviews (to avoid failed forbidden responses, which aren't cached)
const accessReviews = await Promise.all(data.items.map((namespace) => getSelfSubjectAccessReview(this.$state.params.endpointId, namespace.metadata.name)));
const allowedNamespaceNames = accessReviews.filter((ar) => ar.status.allowed).map((ar) => ar.spec.resourceAttributes.namespace);
const promises = allowedNamespaceNames.map((name) => this.KubernetesNamespaces().status({ id: name }).$promise);
const namespaces = await $allSettled(promises);
// only return namespaces if the user has access to namespaces
const allNamespaces = namespaces.fulfilled.map((item) => {
return KubernetesNamespaceConverter.apiToNamespace(item);
});
updateNamespaces(allNamespaces);
return allNamespaces;
// get the list of all namespaces with isAccessAllowed flags
const hasK8sAccessSystemNamespaces = this.Authentication.hasAuthorizations(['K8sAccessSystemNamespaces']);
const namespaces = data.items.filter((item) => !KubernetesNamespaceHelper.isSystemNamespace(item.metadata.name) || hasK8sAccessSystemNamespaces);
// parse the namespaces
const visibleNamespaces = namespaces.map((item) => KubernetesNamespaceConverter.apiToNamespace(item));
updateNamespaces(visibleNamespaces);
return visibleNamespaces;
} catch (err) {
throw new PortainerError('Unable to retrieve namespaces', err);
}

View File

@ -1,12 +1,11 @@
import angular from 'angular';
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 { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
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 {
/* @ngInject */
constructor(
@ -16,6 +15,7 @@ class KubernetesApplicationsController {
Authentication,
Notifications,
KubernetesApplicationService,
EndpointService,
HelmService,
KubernetesConfigurationService,
LocalStorage,
@ -36,8 +36,6 @@ class KubernetesApplicationsController {
this.KubernetesNamespaceService = KubernetesNamespaceService;
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.removeActionAsync = this.removeActionAsync.bind(this);
this.removeStacksAction = this.removeStacksAction.bind(this);
@ -88,23 +86,18 @@ class KubernetesApplicationsController {
if (application.ApplicationType === KubernetesApplicationTypes.Helm) {
await this.HelmService.uninstall(this.endpoint.Id, application);
} else {
await this.KubernetesApplicationService.delete(application);
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
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) {
await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id);
}
}
await this.KubernetesApplicationService.delete(application);
}
this.Notifications.success('Application successfully removed', application.Name);
const index = this.state.applications.indexOf(application);
this.state.applications.splice(index, 1);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to remove application');
} finally {
@ -137,42 +130,15 @@ class KubernetesApplicationsController {
this.state.namespaceName = namespaceName;
// 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);
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) {
return this.$scope.$applyAsync(() => {
this.state.isSystemResources = flag;
});
}
getApplications() {
return this.$async(this.getApplicationsAsync);
}
async onInit() {
this.state = {
activeTab: this.LocalStorage.getActiveTab('applications'),
@ -190,12 +156,12 @@ class KubernetesApplicationsController {
this.deploymentOptions = await getDeploymentOptions();
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 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');
// set all namespaces ('') if there are no namespaces, or if all namespaces is selected
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;
}
await this.getApplications();
this.state.viewReady = true;
}

View File

@ -918,7 +918,7 @@ class KubernetesCreateApplicationController {
async checkIngressesToUpdate() {
let ingressesToUpdate = [];
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) => {
const oldServiceIndex = this.oldFormValues.Services.findIndex((oldService) => oldService.Name === updatedService.Name);
const numberOfPortsInOldService = this.oldFormValues.Services[oldServiceIndex] && this.oldFormValues.Services[oldServiceIndex].Ports.length;

View File

@ -4,7 +4,7 @@ import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesPodConverter from 'Kubernetes/pod/converter';
import { getMetricsForPod } from '@/react/kubernetes/services/service.ts';
import { getMetricsForPod } from '@/react/kubernetes/metrics/metrics.ts';
class KubernetesApplicationStatsController {
/* @ngInject */

View File

@ -3,7 +3,7 @@ import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
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 {
/* @ngInject */
@ -68,20 +68,11 @@ class KubernetesClusterController {
async getApplicationsAsync() {
try {
this.state.applicationsLoading = true;
this.applications = await this.KubernetesApplicationService.get();
const nodeNames = _.map(this.nodes, (node) => node.Name);
this.resourceReservation = _.reduce(
this.applications,
(acc, app) => {
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);
const applicationsResources = await getTotalResourcesForAllApplications(this.endpoint.Id);
this.resourceReservation = new KubernetesResourceReservation();
this.resourceReservation.CPU = Math.round(applicationsResources.CpuRequest / 1000);
this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(applicationsResources.MemoryRequest);
if (this.hasResourceUsageAccess()) {
await this.getResourceUsage(this.endpoint.Id);
@ -133,8 +124,7 @@ class KubernetesClusterController {
await this.getNodes();
if (this.isAdmin) {
await this.getEndpoints();
await this.getApplications();
await Promise.allSettled([this.getEndpoints(), this.getApplicationsAsync()]);
}
this.state.viewReady = true;

View File

@ -76,12 +76,12 @@
<div style="padding: 8px">
<kubernetes-resource-reservation
ng-if="ctrl.resourceReservation"
cpu-reservation="ctrl.resourceReservation.CPU"
cpu-reservation="ctrl.resourceReservation.CpuRequest"
cpu-usage="ctrl.resourceUsage.CPU"
cpu-limit="ctrl.node.CPU"
memory-reservation="ctrl.resourceReservation.Memory"
memory-reservation="ctrl.resourceReservation.MemoryRequest"
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."
display-usage="ctrl.hasResourceUsageAccess()"
>
@ -267,11 +267,5 @@
</div>
</div>
<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>
<kubernetes-node-applications-datatable></kubernetes-node-applications-datatable>
</div>

View File

@ -9,7 +9,7 @@ import { KubernetesNodeTaintEffects, KubernetesNodeAvailabilities } from 'Kubern
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
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 {
/* @ngInject */
@ -40,7 +40,6 @@ class KubernetesNodeController {
this.getNodesAsync = this.getNodesAsync.bind(this);
this.getEvents = this.getEvents.bind(this);
this.getEventsAsync = this.getEventsAsync.bind(this);
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
this.updateNodeAsync = this.updateNodeAsync.bind(this);
this.drainNodeAsync = this.drainNodeAsync.bind(this);
@ -300,6 +299,8 @@ class KubernetesNodeController {
try {
const nodeName = this.$transition$.params().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.CPU = KubernetesResourceReservationHelper.parseCPU(node.usage.cpu);
this.resourceUsage.Memory = KubernetesResourceReservationHelper.megaBytesValue(node.usage.memory);
@ -338,43 +339,6 @@ class KubernetesNodeController {
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() {
this.availabilities = KubernetesNodeAvailabilities;
@ -399,7 +363,6 @@ class KubernetesNodeController {
await this.getNodes();
await this.getEvents();
await this.getApplications();
await this.getEndpoints();
this.availableEffects = _.values(KubernetesNodeTaintEffects);
@ -407,6 +370,11 @@ class KubernetesNodeController {
this.formValues.Labels = KubernetesNodeHelper.computeUsedLabels(this.applications, 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;
}

View File

@ -3,7 +3,7 @@ import moment from 'moment';
import filesizeParser from 'filesize-parser';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import { PORTAINER_FADEOUT } from '@/constants';
import { getMetricsForNode } from '@/react/kubernetes/services/service.ts';
import { getMetricsForNode } from '@/react/kubernetes/metrics/metrics.ts';
class KubernetesNodeStatsController {
/* @ngInject */

Some files were not shown because too many files have changed in this diff Show More