refactor(k8s): optimised the logic for namespace and resource quota fetch for non-admin users

pull/12062/head
stevensbkang 2024-08-14 15:53:37 +12:00
parent 75fb26d3c4
commit 2d85961695
No known key found for this signature in database
4 changed files with 99 additions and 83 deletions

View File

@ -65,7 +65,7 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
endpointRouter.Handle("/rbac_enabled", httperror.LoggerHandler(h.isRBACEnabled)).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.getKubernetesNamespaces)).Methods(http.MethodGet)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.GetKubernetesNamespaces)).Methods(http.MethodGet)
endpointRouter.Handle("/namespaces/count", httperror.LoggerHandler(h.getKubernetesNamespacesCount)).Methods(http.MethodGet)
endpointRouter.Handle("/namespace/{namespace}", httperror.LoggerHandler(h.deleteKubernetesNamespace)).Methods(http.MethodDelete)
endpointRouter.Handle("/namespaces/{namespace}", httperror.LoggerHandler(h.getKubernetesNamespace)).Methods(http.MethodGet)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
models "github.com/portainer/portainer/api/http/models/kubernetes"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
@ -26,36 +27,10 @@ import (
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces [get]
func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
withResourceQuota, err := request.RetrieveBooleanQueryParameter(r, "withResourceQuota", false)
func (handler *Handler) GetKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespaces, err := handler.getKubernetesNamespaces(w, r)
if err != nil {
return httperror.BadRequest("an error occurred during the getKubernetesNamespaces operation, invalid query parameter withResourceQuota. Error: ", err)
}
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
return httperror.InternalServerError("an error occurred during the getKubernetesNamespacesCount operation, unable to get a Kubernetes client for the user. Error: ", httpErr)
}
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return httperror.NotFound("Unable to find an environment on request context", err)
}
pcli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError("an error occurred during the getKubernetesNamespaces operation, unable to get a privileged Kubernetes client for the user. Error: ", err)
}
pcli.IsKubeAdmin = cli.IsKubeAdmin
pcli.NonAdminNamespaces = cli.NonAdminNamespaces
namespaces, err := pcli.GetNamespaces()
if err != nil {
return httperror.InternalServerError("Unable to retrieve namespaces", err)
}
if withResourceQuota {
return pcli.CombineNamespacesWithResourceQuotas(namespaces, w)
return err
}
return response.JSON(w, namespaces)
@ -76,29 +51,47 @@ func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.R
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/count [get]
func (handler *Handler) getKubernetesNamespacesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
namespaces, err := handler.getKubernetesNamespaces(w, r)
if err != nil {
return httperror.NotFound("Unable to find an environment on request context", err)
return err
}
return response.JSON(w, len(namespaces))
}
func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) (map[string]portainer.K8sNamespaceInfo, *httperror.HandlerError) {
withResourceQuota, err := request.RetrieveBooleanQueryParameter(r, "withResourceQuota", false)
if err != nil {
return nil, httperror.BadRequest("an error occurred during the getKubernetesNamespaces operation, invalid query parameter withResourceQuota. Error: ", err)
}
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
return httperror.InternalServerError("an error occurred during the getKubernetesNamespacesCount operation, unable to get a Kubernetes client for the user. Error: ", httpErr)
return nil, httperror.InternalServerError("an error occurred during the getKubernetesNamespacesCount operation, unable to get a Kubernetes client for the user. Error: ", httpErr)
}
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return nil, httperror.NotFound("Unable to find an environment on request context", err)
}
pcli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError("an error occurred during the getKubernetesNamespacesCount operation, unable to get a privileged Kubernetes client for the user. Error: ", err)
return nil, httperror.InternalServerError("an error occurred during the getKubernetesNamespaces operation, unable to get a privileged Kubernetes client for the user. Error: ", err)
}
pcli.IsKubeAdmin = cli.IsKubeAdmin
pcli.NonAdminNamespaces = cli.NonAdminNamespaces
namespaces, err := pcli.GetNamespaces()
if err != nil {
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 nil, httperror.InternalServerError("an error occurred during the getKubernetesNamespaces operation, unable to retrieve namespaces from the Kubernetes cluster. Error: ", err)
}
return response.JSON(w, len(namespaces))
if withResourceQuota {
return pcli.CombineNamespacesWithResourceQuotas(namespaces, w)
}
return namespaces, nil
}
// @id GetKubernetesNamespace

View File

@ -47,28 +47,18 @@ func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, e
// fetchNamespacesForAdmin gets the namespaces in the current k8s environment(endpoint) for the admin user.
// The kube client must have cluster scope read access to do this.
func (kcl *KubeClient) fetchNamespacesForAdmin() (map[string]portainer.K8sNamespaceInfo, error) {
namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("an error occurred during the fetchNamespacesForAdmin operation, unable to list namespaces for the admin user: %w", err)
}
results := make(map[string]portainer.K8sNamespaceInfo)
for _, namespace := range namespaces.Items {
results[namespace.Name] = parseNamespace(&namespace)
}
return results, nil
return kcl.fetchNamespaces()
}
// fetchNamespacesForNonAdmin gets the namespaces in the current k8s environment(endpoint) for the non-admin user.
func (kcl *KubeClient) fetchNamespacesForNonAdmin() (map[string]portainer.K8sNamespaceInfo, error) {
log.Debug().Msgf("Fetching namespaces for non-admin user: %v", kcl.NonAdminNamespaces)
results := make(map[string]portainer.K8sNamespaceInfo)
if len(kcl.NonAdminNamespaces) == 0 {
return results, nil
return nil, nil
}
namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
namespaces, err := kcl.fetchNamespaces()
if err != nil {
return nil, fmt.Errorf("an error occurred during the fetchNamespacesForNonAdmin operation, unable to list namespaces for the non-admin user: %w", err)
}
@ -78,23 +68,31 @@ func (kcl *KubeClient) fetchNamespacesForNonAdmin() (map[string]portainer.K8sNam
nonAdminNamespaceSet[ns] = struct{}{}
}
for _, namespace := range namespaces.Items {
results := make(map[string]portainer.K8sNamespaceInfo)
for _, namespace := range namespaces {
if _, exists := nonAdminNamespaceSet[namespace.Name]; exists {
results[namespace.Name] = parseNamespace(&namespace)
results[namespace.Name] = namespace
}
}
return results, nil
}
// GetNamespace gets the namespace in the current k8s environment(endpoint).
func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, error) {
namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{})
// fetchNamespaces gets the namespaces in the current k8s environment(endpoint).
// this function is used by both admin and non-admin users.
// the result gets parsed to a map of namespace name to namespace info.
func (kcl *KubeClient) fetchNamespaces() (map[string]portainer.K8sNamespaceInfo, error) {
namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return portainer.K8sNamespaceInfo{}, err
return nil, fmt.Errorf("an error occurred during the fetchNamespacesForAdmin operation, unable to list namespaces for the admin user: %w", err)
}
return parseNamespace(namespace), nil
results := make(map[string]portainer.K8sNamespaceInfo)
for _, namespace := range namespaces.Items {
results[namespace.Name] = parseNamespace(&namespace)
}
return results, nil
}
// parseNamespace converts a k8s namespace object to a portainer namespace object.
@ -110,6 +108,16 @@ func parseNamespace(namespace *v1.Namespace) portainer.K8sNamespaceInfo {
}
}
// GetNamespace gets the namespace in the current k8s environment(endpoint).
func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, error) {
namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
return portainer.K8sNamespaceInfo{}, err
}
return parseNamespace(namespace), nil
}
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error {
portainerLabels := map[string]string{
@ -255,17 +263,17 @@ func (kcl *KubeClient) DeleteNamespace(namespace string) error {
}
// CombineNamespacesWithResourceQuotas combines namespaces with resource quotas where matching is based on "portainer-rq-"+namespace.Name
func (kcl *KubeClient) CombineNamespacesWithResourceQuotas(namespaces map[string]portaineree.K8sNamespaceInfo, w http.ResponseWriter) *httperror.HandlerError {
func (kcl *KubeClient) CombineNamespacesWithResourceQuotas(namespaces map[string]portaineree.K8sNamespaceInfo, w http.ResponseWriter) (map[string]portainer.K8sNamespaceInfo, *httperror.HandlerError) {
resourceQuotas, err := kcl.GetResourceQuotas("")
if err != nil {
return httperror.InternalServerError("an error occurred during the CombineNamespacesWithResourceQuotas operation, unable to retrieve resource quotas from the Kubernetes for an admin user. Error: ", err)
return nil, httperror.InternalServerError("an error occurred during the CombineNamespacesWithResourceQuotas operation, unable to retrieve resource quotas from the Kubernetes for an admin user. Error: ", err)
}
if len(*resourceQuotas) > 0 {
return response.JSON(w, kcl.UpdateNamespacesWithResourceQuotas(namespaces, *resourceQuotas))
return kcl.UpdateNamespacesWithResourceQuotas(namespaces, *resourceQuotas), nil
}
return response.JSON(w, namespaces)
return namespaces, nil
}
// CombineNamespaceWithResourceQuota combines a namespace with a resource quota prefixed with "portainer-rq-"+namespace.Name

View File

@ -5,8 +5,8 @@ import (
"fmt"
portaineree "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -14,15 +14,48 @@ import (
// The kube client must have cluster scope read access to do this.
func (kcl *KubeClient) GetResourceQuotas(namespace string) (*[]corev1.ResourceQuota, error) {
if kcl.IsKubeAdmin {
return kcl.fetchResourceQuotasForAdmin()
return kcl.fetchResourceQuotasForAdmin(namespace)
}
return kcl.fetchResourceQuotasForNonAdmin()
return kcl.fetchResourceQuotasForNonAdmin(namespace)
}
// fetchResourceQuotasForAdmin gets the resource quotas in the current k8s environment(endpoint) for an admin user.
// The kube client must have cluster scope read access to do this.
func (kcl *KubeClient) fetchResourceQuotasForAdmin() (*[]corev1.ResourceQuota, error) {
resourceQuotas, err := kcl.cli.CoreV1().ResourceQuotas("").List(context.TODO(), metav1.ListOptions{})
func (kcl *KubeClient) fetchResourceQuotasForAdmin(namespace string) (*[]corev1.ResourceQuota, error) {
return kcl.fetchResourceQuotas(namespace)
}
// fetchResourceQuotasForNonAdmin gets the resource quotas in the current k8s environment(endpoint) for a non-admin user.
// the role of the user must have read access to the resource quotas in the defined namespaces.
func (kcl *KubeClient) fetchResourceQuotasForNonAdmin(namespace string) (*[]corev1.ResourceQuota, error) {
log.Debug().Msgf("Fetching resource quotas for non-admin user: %v", kcl.NonAdminNamespaces)
if len(kcl.NonAdminNamespaces) == 0 {
return nil, nil
}
resourceQuotas, err := kcl.fetchResourceQuotas(namespace)
if err != nil {
return nil, err
}
nonAdminNamespaceSet := make(map[string]struct{}, len(kcl.NonAdminNamespaces))
for _, ns := range kcl.NonAdminNamespaces {
nonAdminNamespaceSet[ns] = struct{}{}
}
results := []corev1.ResourceQuota{}
for _, resourceQuota := range *resourceQuotas {
if _, exists := nonAdminNamespaceSet[resourceQuota.Namespace]; exists {
results = append(results, resourceQuota)
}
}
return &results, nil
}
func (kcl *KubeClient) fetchResourceQuotas(namespace string) (*[]corev1.ResourceQuota, error) {
resourceQuotas, err := kcl.cli.CoreV1().ResourceQuotas(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("an error occured, failed to list resource quotas for the admin user: %w", err)
}
@ -30,24 +63,6 @@ func (kcl *KubeClient) fetchResourceQuotasForAdmin() (*[]corev1.ResourceQuota, e
return &resourceQuotas.Items, nil
}
// fetchResourceQuotasForNonAdmin gets the resource quotas in the current k8s environment(endpoint) for a non-admin user.
// the role of the user must have read access to the resource quotas in the defined namespaces.
func (kcl *KubeClient) fetchResourceQuotasForNonAdmin() (*[]corev1.ResourceQuota, error) {
resourceQuotas := []corev1.ResourceQuota{}
for _, namespace := range kcl.NonAdminNamespaces {
resourceQuota, err := kcl.GetResourceQuota(namespace, "portainer-rq-"+namespace)
if err != nil && !k8serrors.IsNotFound(err) {
return nil, fmt.Errorf("an error occured, failed to get a resource quota %s of the namespace %s for the non-admin user: %w", resourceQuota.Name, namespace, err)
}
if resourceQuota.Namespace != "" && resourceQuota.Name != "" {
resourceQuotas = append(resourceQuotas, *resourceQuota)
}
}
return &resourceQuotas, nil
}
// GetPortainerResourceQuota gets the resource quota for the portainer namespace.
// The resource quota is prefixed with "portainer-rq-".
func (kcl *KubeClient) GetPortainerResourceQuota(namespace string) (*corev1.ResourceQuota, error) {