From 2d859616952209d9d0e5505bd21102b2d774c8be Mon Sep 17 00:00:00 2001 From: stevensbkang Date: Wed, 14 Aug 2024 15:53:37 +1200 Subject: [PATCH] refactor(k8s): optimised the logic for namespace and resource quota fetch for non-admin users --- api/http/handler/kubernetes/handler.go | 2 +- api/http/handler/kubernetes/namespaces.go | 63 ++++++++++------------- api/kubernetes/cli/namespace.go | 56 +++++++++++--------- api/kubernetes/cli/resource_quota.go | 61 +++++++++++++--------- 4 files changed, 99 insertions(+), 83 deletions(-) diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go index 70687305c..ac53b8297 100644 --- a/api/http/handler/kubernetes/handler.go +++ b/api/http/handler/kubernetes/handler.go @@ -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) diff --git a/api/http/handler/kubernetes/namespaces.go b/api/http/handler/kubernetes/namespaces.go index 0fa9fb961..79461c4ab 100644 --- a/api/http/handler/kubernetes/namespaces.go +++ b/api/http/handler/kubernetes/namespaces.go @@ -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 diff --git a/api/kubernetes/cli/namespace.go b/api/kubernetes/cli/namespace.go index 98afe4beb..e9f6888eb 100644 --- a/api/kubernetes/cli/namespace.go +++ b/api/kubernetes/cli/namespace.go @@ -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 diff --git a/api/kubernetes/cli/resource_quota.go b/api/kubernetes/cli/resource_quota.go index 0614be2c0..4d87e2a0c 100644 --- a/api/kubernetes/cli/resource_quota.go +++ b/api/kubernetes/cli/resource_quota.go @@ -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) {