1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +02:00

refactor(k8s): namespace core logic (#12142)

Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: James Carppe <85850129+jamescarppe@users.noreply.github.com>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
This commit is contained in:
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

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