From 6d31f4876a9fdab7c0e51f861bfbfe4071a84975 Mon Sep 17 00:00:00 2001 From: Yajith Dayarathna Date: Tue, 12 Nov 2024 09:55:30 +1300 Subject: [PATCH] fix(more resources): fix porting and functionality [r8s-103] (#8) Co-authored-by: testA113 Co-authored-by: Anthony Lapenna Co-authored-by: Ali <83188384+testA113@users.noreply.github.com> --- .../kubernetes/cluster_role_bindings.go | 38 ++++++++ api/http/handler/kubernetes/cluster_roles.go | 38 ++++++++ api/http/handler/kubernetes/handler.go | 15 +++- api/http/handler/kubernetes/role_bindings.go | 37 ++++++++ api/http/handler/kubernetes/roles.go | 38 ++++++++ .../handler/kubernetes/service_accounts.go | 38 ++++++++ .../kubernetes/cluster_role_bindings.go | 17 ++++ api/http/models/kubernetes/cluster_roles.go | 28 +++++- api/http/models/kubernetes/role_bindings.go | 21 +++++ api/http/models/kubernetes/roles.go | 37 ++++++-- .../models/kubernetes/service_accounts.go | 35 ++++++-- api/internal/errorlist/errorlist.go | 18 ++++ api/kubernetes/cli/cluster_role.go | 64 +++++++++++++- api/kubernetes/cli/cluster_role_binding.go | 66 ++++++++++++++ api/kubernetes/cli/namespace.go | 15 +++- api/kubernetes/cli/namespace_test.go | 2 +- api/kubernetes/cli/role.go | 55 +++++++++++- api/kubernetes/cli/role_binding.go | 73 +++++++++++++++- api/kubernetes/cli/service_account.go | 43 ++++++++- api/portainer.go | 12 +++ app/react/components/Badge/SystemBadge.tsx | 2 +- .../ClusterRoleBindingsDatatable.tsx | 15 ++-- ...ingsQuery.ts => useClusterRoleBindings.ts} | 2 +- ...ion.ts => useDeleteClusterRoleBindings.ts} | 4 +- .../ClusterRoleBindingsDatatable/types.ts | 6 +- .../ClusterRolesDatatable.tsx | 77 +++++++++++++--- .../ClusterRolesDatatable/columns/helper.ts | 4 +- .../ClusterRolesDatatable/columns/name.tsx | 3 - ...lusterRolesQuery.ts => useClusterRoles.ts} | 2 +- ...esMutation.ts => useDeleteClusterRoles.ts} | 2 +- .../ClusterRolesDatatable/types.ts | 17 +--- .../ClusterRolesView/ClusterRolesView.tsx | 5 +- .../RoleBindingsDatatable.tsx | 52 ++++++----- ...gsMutation.ts => useDeleteRoleBindings.ts} | 2 +- ...oleBindingsQuery.ts => useRoleBindings.ts} | 2 +- .../RolesView/RoleBindingsDatatable/types.ts | 6 +- .../RolesDatatable/RolesDatatable.tsx | 87 +++++++++++++------ .../RolesDatatable/columns/helper.ts | 4 +- .../RolesDatatable/columns/namespace.tsx | 4 +- ...leteRolesMutation.ts => useDeleteRoles.ts} | 2 +- .../{useGetAllRolesQuery.ts => useRoles.ts} | 2 +- .../RolesView/RolesDatatable/types.ts | 8 +- .../more-resources/RolesView/RolesView.tsx | 2 +- .../ServiceAccountsDatatable.tsx | 52 ++++++----- .../columns/index.tsx | 4 +- .../ServiceAccountsDatatable/columns/name.tsx | 5 -- .../ServiceAccountsView.tsx | 6 ++ .../ServiceAccountsView/types.ts | 5 +- 48 files changed, 890 insertions(+), 182 deletions(-) create mode 100644 api/internal/errorlist/errorlist.go rename app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/{useGetClusterRoleBindingsQuery.ts => useClusterRoleBindings.ts} (95%) rename app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/{useDeleteClusterRoleBindingsMutation.ts => useDeleteClusterRoleBindings.ts} (91%) rename app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/{useGetClusterRolesQuery.ts => useClusterRoles.ts} (96%) rename app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/{useDeleteClusterRolesMutation.ts => useDeleteClusterRoles.ts} (91%) rename app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/{useDeleteRoleBindingsMutation.ts => useDeleteRoleBindings.ts} (91%) rename app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/{useGetAllRoleBindingsQuery.ts => useRoleBindings.ts} (95%) rename app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/{useDeleteRolesMutation.ts => useDeleteRoles.ts} (92%) rename app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/{useGetAllRolesQuery.ts => useRoles.ts} (96%) diff --git a/api/http/handler/kubernetes/cluster_role_bindings.go b/api/http/handler/kubernetes/cluster_role_bindings.go index 918c4950d..a5050c947 100644 --- a/api/http/handler/kubernetes/cluster_role_bindings.go +++ b/api/http/handler/kubernetes/cluster_role_bindings.go @@ -3,7 +3,9 @@ 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" ) @@ -43,3 +45,39 @@ func (handler *Handler) getAllKubernetesClusterRoleBindings(w http.ResponseWrite return response.JSON(w, clusterrolebindings) } + +// @id DeleteClusterRoleBindings +// @summary Delete cluster role bindings +// @description Delete the provided list of cluster role bindings. +// @description **Access policy**: Authenticated user. +// @tags kubernetes +// @security ApiKeyAuth || jwt +// @accept json +// @param id path int true "Environment identifier" +// @param payload body models.K8sClusterRoleBindingDeleteRequests true "A list of cluster role bindings to delete" +// @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 cluster role binding." +// @failure 500 "Server error occurred while attempting to delete cluster role bindings." +// @router /kubernetes/{id}/cluster_role_bindings/delete [POST] +func (handler *Handler) deleteClusterRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload models.K8sClusterRoleBindingDeleteRequests + 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.DeleteClusterRoleBindings(payload) + if err != nil { + return httperror.InternalServerError("Failed to delete cluster role bindings", err) + } + + return response.Empty(w) +} diff --git a/api/http/handler/kubernetes/cluster_roles.go b/api/http/handler/kubernetes/cluster_roles.go index 0cf226bd4..3fd2ca8aa 100644 --- a/api/http/handler/kubernetes/cluster_roles.go +++ b/api/http/handler/kubernetes/cluster_roles.go @@ -3,7 +3,9 @@ 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" ) @@ -43,3 +45,39 @@ func (handler *Handler) getAllKubernetesClusterRoles(w http.ResponseWriter, r *h return response.JSON(w, clusterroles) } + +// @id DeleteClusterRoles +// @summary Delete cluster roles +// @description Delete the provided list of cluster roles. +// @description **Access policy**: Authenticated user. +// @tags kubernetes +// @security ApiKeyAuth || jwt +// @accept json +// @param id path int true "Environment identifier" +// @param payload body models.K8sClusterRoleDeleteRequests true "A list of cluster roles to delete" +// @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 cluster role." +// @failure 500 "Server error occurred while attempting to delete cluster roles." +// @router /kubernetes/{id}/cluster_roles/delete [POST] +func (handler *Handler) deleteClusterRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload models.K8sClusterRoleDeleteRequests + 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.DeleteClusterRoles(payload) + if err != nil { + return httperror.InternalServerError("Failed to delete cluster roles", err) + } + + return response.Empty(w) +} diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go index 778f569d2..d3ae22f73 100644 --- a/api/http/handler/kubernetes/handler.go +++ b/api/http/handler/kubernetes/handler.go @@ -56,7 +56,9 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza 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_roles/delete", httperror.LoggerHandler(h.deleteClusterRoles)).Methods(http.MethodPost) endpointRouter.Handle("/cluster_role_bindings", httperror.LoggerHandler(h.getAllKubernetesClusterRoleBindings)).Methods(http.MethodGet) + endpointRouter.Handle("/cluster_role_bindings/delete", httperror.LoggerHandler(h.deleteClusterRoleBindings)).Methods(http.MethodPost) 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) @@ -72,15 +74,12 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza 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.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) @@ -89,6 +88,16 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza 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) + endpointRouter.Handle("/service_accounts", httperror.LoggerHandler(h.getAllKubernetesServiceAccounts)).Methods(http.MethodGet) + endpointRouter.Handle("/service_accounts/delete", httperror.LoggerHandler(h.deleteKubernetesServiceAccounts)).Methods(http.MethodPost) + endpointRouter.Handle("/roles", httperror.LoggerHandler(h.getAllKubernetesRoles)).Methods(http.MethodGet) + endpointRouter.Handle("/roles/delete", httperror.LoggerHandler(h.deleteRoles)).Methods(http.MethodPost) + endpointRouter.Handle("/role_bindings", httperror.LoggerHandler(h.getAllKubernetesRoleBindings)).Methods(http.MethodGet) + endpointRouter.Handle("/role_bindings/delete", httperror.LoggerHandler(h.deleteRoleBindings)).Methods(http.MethodPost) + endpointRouter.Handle("/cluster_roles", httperror.LoggerHandler(h.getAllKubernetesClusterRoles)).Methods(http.MethodGet) + endpointRouter.Handle("/cluster_roles/delete", httperror.LoggerHandler(h.deleteClusterRoles)).Methods(http.MethodPost) + endpointRouter.Handle("/cluster_role_bindings", httperror.LoggerHandler(h.getAllKubernetesClusterRoleBindings)).Methods(http.MethodGet) + endpointRouter.Handle("/cluster_role_bindings/delete", httperror.LoggerHandler(h.deleteClusterRoleBindings)).Methods(http.MethodPost) // namespaces // in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?) diff --git a/api/http/handler/kubernetes/role_bindings.go b/api/http/handler/kubernetes/role_bindings.go index 6c58f4b50..0f5260b77 100644 --- a/api/http/handler/kubernetes/role_bindings.go +++ b/api/http/handler/kubernetes/role_bindings.go @@ -3,7 +3,9 @@ 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" ) @@ -38,3 +40,38 @@ func (handler *Handler) getAllKubernetesRoleBindings(w http.ResponseWriter, r *h return response.JSON(w, rolebindings) } + +// @id DeleteRoleBindings +// @summary Delete role bindings +// @description Delete the provided list of role bindings. +// @description **Access policy**: Authenticated user. +// @tags kubernetes +// @security ApiKeyAuth || jwt +// @accept json +// @param id path int true "Environment identifier" +// @param payload body models.K8sRoleBindingDeleteRequests true "A map where the key is the namespace and the value is an array of role bindings to delete" +// @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 role binding." +// @failure 500 "Server error occurred while attempting to delete role bindings." +// @router /kubernetes/{id}/role_bindings/delete [POST] +func (h *Handler) deleteRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload models.K8sRoleBindingDeleteRequests + + if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil { + return httperror.BadRequest("Invalid request payload", err) + } + + cli, handlerErr := h.getProxyKubeClient(r) + if handlerErr != nil { + return handlerErr + } + + if err := cli.DeleteRoleBindings(payload); err != nil { + return httperror.InternalServerError("Failed to delete role bindings", err) + } + + return response.Empty(w) +} diff --git a/api/http/handler/kubernetes/roles.go b/api/http/handler/kubernetes/roles.go index f44309ff2..65ae35318 100644 --- a/api/http/handler/kubernetes/roles.go +++ b/api/http/handler/kubernetes/roles.go @@ -3,7 +3,9 @@ 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" ) @@ -38,3 +40,39 @@ func (handler *Handler) getAllKubernetesRoles(w http.ResponseWriter, r *http.Req return response.JSON(w, roles) } + +// @id DeleteRoles +// @summary Delete roles +// @description Delete the provided list of roles. +// @description **Access policy**: Authenticated user. +// @tags kubernetes +// @security ApiKeyAuth || jwt +// @accept json +// @param id path int true "Environment identifier" +// @param payload body models.K8sRoleDeleteRequests true "A map where the key is the namespace and the value is an array of roles to delete" +// @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 role." +// @failure 500 "Server error occurred while attempting to delete roles." +// @router /kubernetes/{id}/roles/delete [POST] +func (h *Handler) deleteRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload models.K8sRoleDeleteRequests + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return httperror.BadRequest("Invalid request payload", err) + } + + cli, handlerErr := h.getProxyKubeClient(r) + if handlerErr != nil { + return handlerErr + } + + err = cli.DeleteRoles(payload) + if err != nil { + return httperror.InternalServerError("Failed to delete roles", err) + } + + return response.Empty(w) +} diff --git a/api/http/handler/kubernetes/service_accounts.go b/api/http/handler/kubernetes/service_accounts.go index 424634804..31527b0ee 100644 --- a/api/http/handler/kubernetes/service_accounts.go +++ b/api/http/handler/kubernetes/service_accounts.go @@ -3,7 +3,9 @@ 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" ) @@ -38,3 +40,39 @@ func (handler *Handler) getAllKubernetesServiceAccounts(w http.ResponseWriter, r return response.JSON(w, serviceAccounts) } + +// @id DeleteServiceAccounts +// @summary Delete service accounts +// @description Delete the provided list of service accounts. +// @description **Access policy**: Authenticated user. +// @tags kubernetes +// @security ApiKeyAuth || jwt +// @accept json +// @param id path int true "Environment identifier" +// @param payload body models.K8sServiceAccountDeleteRequests true "A map where the key is the namespace and the value is an array of service accounts to delete" +// @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 account." +// @failure 500 "Server error occurred while attempting to delete service accounts." +// @router /kubernetes/{id}/service_accounts/delete [POST] +func (handler *Handler) deleteKubernetesServiceAccounts(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload models.K8sServiceAccountDeleteRequests + 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.DeleteServiceAccounts(payload) + if err != nil { + return httperror.InternalServerError("Unable to delete service accounts", err) + } + + return response.Empty(w) +} diff --git a/api/http/models/kubernetes/cluster_role_bindings.go b/api/http/models/kubernetes/cluster_role_bindings.go index 5a709dcac..546973937 100644 --- a/api/http/models/kubernetes/cluster_role_bindings.go +++ b/api/http/models/kubernetes/cluster_role_bindings.go @@ -1,16 +1,33 @@ package kubernetes import ( + "errors" + "net/http" "time" rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" ) type ( K8sClusterRoleBinding struct { Name string `json:"name"` + UID types.UID `json:"uid"` + Namespace string `json:"namespace"` RoleRef rbacv1.RoleRef `json:"roleRef"` Subjects []rbacv1.Subject `json:"subjects"` CreationDate time.Time `json:"creationDate"` + IsSystem bool `json:"isSystem"` } + + // K8sRoleBindingDeleteRequests slice of cluster role cluster bindings. + K8sClusterRoleBindingDeleteRequests []string ) + +func (r K8sClusterRoleBindingDeleteRequests) Validate(request *http.Request) error { + if len(r) == 0 { + return errors.New("missing deletion request list in payload") + } + + return nil +} diff --git a/api/http/models/kubernetes/cluster_roles.go b/api/http/models/kubernetes/cluster_roles.go index 78fcd909e..a42b19474 100644 --- a/api/http/models/kubernetes/cluster_roles.go +++ b/api/http/models/kubernetes/cluster_roles.go @@ -1,8 +1,28 @@ package kubernetes -import "time" +import ( + "errors" + "net/http" + "time" -type K8sClusterRole struct { - Name string `json:"name"` - CreationDate time.Time `json:"creationDate"` + "k8s.io/apimachinery/pkg/types" +) + +type ( + K8sClusterRole struct { + Name string `json:"name"` + UID types.UID `json:"uid"` + CreationDate time.Time `json:"creationDate"` + IsSystem bool `json:"isSystem"` + } + + K8sClusterRoleDeleteRequests []string +) + +func (r K8sClusterRoleDeleteRequests) Validate(request *http.Request) error { + if len(r) == 0 { + return errors.New("missing deletion request list in payload") + } + + return nil } diff --git a/api/http/models/kubernetes/role_bindings.go b/api/http/models/kubernetes/role_bindings.go index 7f75dd191..2078244eb 100644 --- a/api/http/models/kubernetes/role_bindings.go +++ b/api/http/models/kubernetes/role_bindings.go @@ -1,17 +1,38 @@ package kubernetes import ( + "errors" + "net/http" "time" rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" ) type ( K8sRoleBinding struct { Name string `json:"name"` + UID types.UID `json:"uid"` Namespace string `json:"namespace"` RoleRef rbacv1.RoleRef `json:"roleRef"` Subjects []rbacv1.Subject `json:"subjects"` CreationDate time.Time `json:"creationDate"` + IsSystem bool `json:"isSystem"` } + + // K8sRoleBindingDeleteRequests is a mapping of namespace names to a slice of role bindings. + K8sRoleBindingDeleteRequests map[string][]string ) + +func (r K8sRoleBindingDeleteRequests) Validate(request *http.Request) error { + if len(r) == 0 { + return errors.New("missing deletion request list in payload") + } + + for ns := range r { + if len(ns) == 0 { + return errors.New("deletion given with empty namespace") + } + } + return nil +} diff --git a/api/http/models/kubernetes/roles.go b/api/http/models/kubernetes/roles.go index 65757def3..aba205b21 100644 --- a/api/http/models/kubernetes/roles.go +++ b/api/http/models/kubernetes/roles.go @@ -1,9 +1,36 @@ package kubernetes -import "time" +import ( + "errors" + "net/http" + "time" -type K8sRole struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - CreationDate time.Time `json:"creationDate"` + "k8s.io/apimachinery/pkg/types" +) + +type ( + K8sRole struct { + Name string `json:"name"` + UID types.UID `json:"uid"` + Namespace string `json:"namespace"` + CreationDate time.Time `json:"creationDate"` + // isSystem is true if prefixed with "system:" or exists in the kube-system namespace + // or is one of the portainer roles + IsSystem bool `json:"isSystem"` + } + + // K8sRoleDeleteRequests is a mapping of namespace names to a slice of roles. + K8sRoleDeleteRequests map[string][]string +) + +func (r K8sRoleDeleteRequests) Validate(request *http.Request) error { + if len(r) == 0 { + return errors.New("missing deletion request list in payload") + } + for ns := range r { + if len(ns) == 0 { + return errors.New("deletion given with empty namespace") + } + } + return nil } diff --git a/api/http/models/kubernetes/service_accounts.go b/api/http/models/kubernetes/service_accounts.go index 533d4a986..4540ffb55 100644 --- a/api/http/models/kubernetes/service_accounts.go +++ b/api/http/models/kubernetes/service_accounts.go @@ -1,9 +1,34 @@ package kubernetes -import "time" +import ( + "errors" + "net/http" + "time" -type K8sServiceAccount struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - CreationDate time.Time `json:"creationDate"` + "k8s.io/apimachinery/pkg/types" +) + +type ( + K8sServiceAccount struct { + Name string `json:"name"` + UID types.UID `json:"uid"` + Namespace string `json:"namespace"` + CreationDate time.Time `json:"creationDate"` + IsSystem bool `json:"isSystem"` + } + + // K8sServiceAcountDeleteRequests is a mapping of namespace names to a slice of service account names. + K8sServiceAccountDeleteRequests map[string][]string +) + +func (r K8sServiceAccountDeleteRequests) Validate(request *http.Request) error { + if len(r) == 0 { + return errors.New("missing deletion request list in payload") + } + for ns := range r { + if len(ns) == 0 { + return errors.New("deletion given with empty namespace") + } + } + return nil } diff --git a/api/internal/errorlist/errorlist.go b/api/internal/errorlist/errorlist.go new file mode 100644 index 000000000..79b5eea18 --- /dev/null +++ b/api/internal/errorlist/errorlist.go @@ -0,0 +1,18 @@ +package errorlist + +import "errors" + +// Combine a slice of errors into a single error +// to use this, generate errors by appending to errorList in a loop, then return combine(errorList) +func Combine(errorList []error) error { + if len(errorList) == 0 { + return nil + } + + errorMsg := "Multiple errors occurred:" + for _, err := range errorList { + errorMsg += "\n" + err.Error() + } + + return errors.New(errorMsg) +} diff --git a/api/kubernetes/cli/cluster_role.go b/api/kubernetes/cli/cluster_role.go index 17a59dc08..8c375d727 100644 --- a/api/kubernetes/cli/cluster_role.go +++ b/api/kubernetes/cli/cluster_role.go @@ -3,10 +3,14 @@ package cli import ( "context" "errors" + "strings" models "github.com/portainer/portainer/api/http/models/kubernetes" + "github.com/portainer/portainer/api/internal/errorlist" + "github.com/rs/zerolog/log" rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" ) // GetClusterRoles gets all the clusterRoles for at the cluster level in a k8s endpoint. @@ -21,7 +25,7 @@ func (kcl *KubeClient) GetClusterRoles() ([]models.K8sClusterRole, error) { // fetchClusterRoles returns a list of all Roles in the specified namespace. func (kcl *KubeClient) fetchClusterRoles() ([]models.K8sClusterRole, error) { - clusterRoles, err := kcl.cli.RbacV1().ClusterRoles().List(context.TODO(), metav1.ListOptions{}) + clusterRoles, err := kcl.cli.RbacV1().ClusterRoles().List(context.TODO(), meta.ListOptions{}) if err != nil { return nil, err } @@ -39,5 +43,61 @@ func parseClusterRole(clusterRole rbacv1.ClusterRole) models.K8sClusterRole { return models.K8sClusterRole{ Name: clusterRole.Name, CreationDate: clusterRole.CreationTimestamp.Time, + UID: clusterRole.UID, + IsSystem: isSystemClusterRole(&clusterRole), } } + +func (kcl *KubeClient) DeleteClusterRoles(req models.K8sClusterRoleDeleteRequests) error { + var errors []error + for _, name := range req { + client := kcl.cli.RbacV1().ClusterRoles() + + clusterRole, err := client.Get(context.Background(), name, meta.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + continue + } + // this is a more serious error to do with the client so we return right away + return err + } + + if isSystemClusterRole(clusterRole) { + log.Warn().Str("role_name", name).Msg("ignoring delete of 'system' cluster role, not allowed") + } + + err = client.Delete(context.Background(), name, meta.DeleteOptions{}) + if err != nil { + log.Err(err).Str("role_name", name).Msg("unable to delete the cluster role") + errors = append(errors, err) + } + } + + return errorlist.Combine(errors) +} + +func isSystemClusterRole(role *rbacv1.ClusterRole) bool { + if role.Namespace == "kube-system" || role.Namespace == "kube-public" || + role.Namespace == "kube-node-lease" || role.Namespace == "portainer" { + return true + } + + if strings.HasPrefix(role.Name, "system:") { + return true + } + + if role.Labels != nil { + if role.Labels["kubernetes.io/bootstrapping"] == "rbac-defaults" { + return true + } + } + + roles := getPortainerDefaultK8sRoleNames() + for i := range roles { + if role.Name == roles[i] { + return true + } + } + + return false +} diff --git a/api/kubernetes/cli/cluster_role_binding.go b/api/kubernetes/cli/cluster_role_binding.go index 2a4e84a15..1c0824e6c 100644 --- a/api/kubernetes/cli/cluster_role_binding.go +++ b/api/kubernetes/cli/cluster_role_binding.go @@ -3,9 +3,13 @@ package cli import ( "context" "errors" + "strings" models "github.com/portainer/portainer/api/http/models/kubernetes" + "github.com/portainer/portainer/api/internal/errorlist" + "github.com/rs/zerolog/log" rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -38,8 +42,70 @@ func (kcl *KubeClient) fetchClusterRoleBindings() ([]models.K8sClusterRoleBindin func parseClusterRoleBinding(clusterRoleBinding rbacv1.ClusterRoleBinding) models.K8sClusterRoleBinding { return models.K8sClusterRoleBinding{ Name: clusterRoleBinding.Name, + UID: clusterRoleBinding.UID, + Namespace: clusterRoleBinding.Namespace, RoleRef: clusterRoleBinding.RoleRef, Subjects: clusterRoleBinding.Subjects, CreationDate: clusterRoleBinding.CreationTimestamp.Time, + IsSystem: isSystemClusterRoleBinding(&clusterRoleBinding), } } + +// DeleteClusterRoleBindings processes a K8sClusterRoleBindingDeleteRequest +// by deleting each cluster role binding in its given namespace. If deleting a specific cluster role binding +// fails, the error is logged and we continue to delete the remaining cluster role bindings. +func (kcl *KubeClient) DeleteClusterRoleBindings(reqs models.K8sClusterRoleBindingDeleteRequests) error { + var errors []error + + for _, name := range reqs { + client := kcl.cli.RbacV1().ClusterRoleBindings() + + clusterRoleBinding, err := client.Get(context.Background(), name, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + continue + } + + // This is a more serious error to do with the client so we return right away + return err + } + + if isSystemClusterRoleBinding(clusterRoleBinding) { + log.Warn().Str("role_name", name).Msg("ignoring delete of 'system' cluster role binding, not allowed") + } + + if err := client.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil { + log.Err(err).Str("role_name", name).Msg("unable to delete the cluster role binding") + errors = append(errors, err) + } + } + + return errorlist.Combine(errors) +} + +func isSystemClusterRoleBinding(binding *rbacv1.ClusterRoleBinding) bool { + if strings.HasPrefix(binding.Name, "system:") { + return true + } + + if binding.Labels != nil { + if binding.Labels["kubernetes.io/bootstrapping"] == "rbac-defaults" { + return true + } + } + + for _, sub := range binding.Subjects { + if strings.HasPrefix(sub.Name, "system:") { + return true + } + + if sub.Namespace == "kube-system" || + sub.Namespace == "kube-public" || + sub.Namespace == "kube-node-lease" || + sub.Namespace == "portainer" { + return true + } + } + + return false +} diff --git a/api/kubernetes/cli/namespace.go b/api/kubernetes/cli/namespace.go index 57cd759cd..24c6cab60 100644 --- a/api/kubernetes/cli/namespace.go +++ b/api/kubernetes/cli/namespace.go @@ -94,7 +94,7 @@ func parseNamespace(namespace *corev1.Namespace) portainer.K8sNamespaceInfo { Status: namespace.Status, CreationDate: namespace.CreationTimestamp.Format(time.RFC3339), NamespaceOwner: namespace.Labels[namespaceOwnerLabel], - IsSystem: isSystemNamespace(*namespace), + IsSystem: isSystemNamespace(namespace), IsDefault: namespace.Name == defaultNamespace, } } @@ -171,7 +171,7 @@ func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) (*corev1 return namespace, nil } -func isSystemNamespace(namespace corev1.Namespace) bool { +func isSystemNamespace(namespace *corev1.Namespace) bool { systemLabelValue, hasSystemLabel := namespace.Labels[systemNamespaceLabel] if hasSystemLabel { return systemLabelValue == "true" @@ -184,6 +184,15 @@ func isSystemNamespace(namespace corev1.Namespace) bool { return isSystem } +func (kcl *KubeClient) isSystemNamespace(namespace string) bool { + ns, err := kcl.cli.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}) + if err != nil { + return false + } + + return isSystemNamespace(ns) +} + // ToggleSystemState will set a namespace as a system namespace, or remove this state // if isSystem is true it will set `systemNamespaceLabel` to "true" and false otherwise // this will skip if namespace is "default" or if the required state is already set @@ -199,7 +208,7 @@ func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) er return errors.Wrap(err, "failed fetching namespace object") } - if isSystemNamespace(*namespace) == isSystem { + if isSystemNamespace(namespace) == isSystem { return nil } diff --git a/api/kubernetes/cli/namespace_test.go b/api/kubernetes/cli/namespace_test.go index bd7a6a5c7..d3d25a4d0 100644 --- a/api/kubernetes/cli/namespace_test.go +++ b/api/kubernetes/cli/namespace_test.go @@ -65,7 +65,7 @@ func Test_ToggleSystemState(t *testing.T) { ns, err := kcl.cli.CoreV1().Namespaces().Get(context.Background(), nsName, metav1.GetOptions{}) assert.NoError(t, err) - assert.Equal(t, test.isSystem, isSystemNamespace(*ns)) + assert.Equal(t, test.isSystem, isSystemNamespace(ns)) }) } }) diff --git a/api/kubernetes/cli/role.go b/api/kubernetes/cli/role.go index 3460f45dc..9a3c6635f 100644 --- a/api/kubernetes/cli/role.go +++ b/api/kubernetes/cli/role.go @@ -2,11 +2,15 @@ package cli import ( "context" + "strings" models "github.com/portainer/portainer/api/http/models/kubernetes" + "github.com/portainer/portainer/api/internal/errorlist" + "github.com/rs/zerolog/log" rbacv1 "k8s.io/api/rbac/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // GetRoles gets all the roles for either at the cluster level or a given namespace in a k8s endpoint. @@ -48,18 +52,20 @@ func (kcl *KubeClient) fetchRoles(namespace string) ([]models.K8sRole, error) { results := make([]models.K8sRole, 0) for _, role := range roles.Items { - results = append(results, parseRole(role)) + results = append(results, kcl.parseRole(role)) } return results, nil } // parseRole converts a rbacv1.Role object to a models.K8sRole object. -func parseRole(role rbacv1.Role) models.K8sRole { +func (kcl *KubeClient) parseRole(role rbacv1.Role) models.K8sRole { return models.K8sRole{ Name: role.Name, + UID: role.UID, Namespace: role.Namespace, CreationDate: role.CreationTimestamp.Time, + IsSystem: kcl.isSystemRole(&role), } } @@ -108,3 +114,48 @@ func (kcl *KubeClient) upsertPortainerK8sClusterRoles() error { return nil } + +func getPortainerDefaultK8sRoleNames() []string { + return []string{ + string(portainerUserCRName), + } +} + +func (kcl *KubeClient) isSystemRole(role *rbacv1.Role) bool { + if strings.HasPrefix(role.Name, "system:") { + return true + } + + return kcl.isSystemNamespace(role.Namespace) +} + +// DeleteRoles processes a K8sServiceDeleteRequest by deleting each role +// in its given namespace. +func (kcl *KubeClient) DeleteRoles(reqs models.K8sRoleDeleteRequests) error { + var errors []error + for namespace := range reqs { + for _, name := range reqs[namespace] { + client := kcl.cli.RbacV1().Roles(namespace) + + role, err := client.Get(context.Background(), name, v1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + continue + } + + // This is a more serious error to do with the client so we return right away + return err + } + + if kcl.isSystemRole(role) { + log.Error().Str("role_name", name).Msg("ignoring delete of 'system' role, not allowed") + } + + if err := client.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil { + errors = append(errors, err) + } + } + } + + return errorlist.Combine(errors) +} diff --git a/api/kubernetes/cli/role_binding.go b/api/kubernetes/cli/role_binding.go index 09d4f813b..e8e90cbb0 100644 --- a/api/kubernetes/cli/role_binding.go +++ b/api/kubernetes/cli/role_binding.go @@ -2,10 +2,16 @@ package cli import ( "context" + "strings" models "github.com/portainer/portainer/api/http/models/kubernetes" + "github.com/portainer/portainer/api/internal/errorlist" + "github.com/rs/zerolog/log" + corev1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // GetRoleBindings gets all the roleBindings for either at the cluster level or a given namespace in a k8s endpoint. @@ -47,19 +53,82 @@ func (kcl *KubeClient) fetchRoleBindings(namespace string) ([]models.K8sRoleBind results := make([]models.K8sRoleBinding, 0) for _, roleBinding := range roleBindings.Items { - results = append(results, parseRoleBinding(roleBinding)) + results = append(results, kcl.parseRoleBinding(roleBinding)) } return results, nil } // parseRoleBinding converts a rbacv1.RoleBinding object to a models.K8sRoleBinding object. -func parseRoleBinding(roleBinding rbacv1.RoleBinding) models.K8sRoleBinding { +func (kcl *KubeClient) parseRoleBinding(roleBinding rbacv1.RoleBinding) models.K8sRoleBinding { return models.K8sRoleBinding{ Name: roleBinding.Name, + UID: roleBinding.UID, Namespace: roleBinding.Namespace, RoleRef: roleBinding.RoleRef, Subjects: roleBinding.Subjects, CreationDate: roleBinding.CreationTimestamp.Time, + IsSystem: kcl.isSystemRoleBinding(&roleBinding), } } + +func (kcl *KubeClient) isSystemRoleBinding(rb *rbacv1.RoleBinding) bool { + if strings.HasPrefix(rb.Name, "system:") { + return true + } + + if rb.Labels != nil { + if rb.Labels["kubernetes.io/bootstrapping"] == "rbac-defaults" { + return true + } + } + + if rb.RoleRef.Name != "" { + role, err := kcl.getRole(rb.Namespace, rb.RoleRef.Name) + if err != nil { + return false + } + + // Linked to a role that is marked a system role + if kcl.isSystemRole(role) { + return true + } + } + + return false +} + +func (kcl *KubeClient) getRole(namespace, name string) (*corev1.Role, error) { + client := kcl.cli.RbacV1().Roles(namespace) + return client.Get(context.Background(), name, metav1.GetOptions{}) +} + +// DeleteRoleBindings processes a K8sServiceDeleteRequest by deleting each service +// in its given namespace. +func (kcl *KubeClient) DeleteRoleBindings(reqs models.K8sRoleBindingDeleteRequests) error { + var errors []error + for namespace := range reqs { + for _, name := range reqs[namespace] { + client := kcl.cli.RbacV1().RoleBindings(namespace) + + roleBinding, err := client.Get(context.Background(), name, v1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + continue + } + + // This is a more serious error to do with the client so we return right away + return err + } + + if kcl.isSystemRoleBinding(roleBinding) { + log.Error().Str("role_name", name).Msg("ignoring delete of 'system' role binding, not allowed") + } + + if err := client.Delete(context.Background(), name, v1.DeleteOptions{}); err != nil { + errors = append(errors, err) + } + } + } + return errorlist.Combine(errors) +} diff --git a/api/kubernetes/cli/service_account.go b/api/kubernetes/cli/service_account.go index f0b2679ab..831080efc 100644 --- a/api/kubernetes/cli/service_account.go +++ b/api/kubernetes/cli/service_account.go @@ -2,9 +2,12 @@ package cli import ( "context" + "fmt" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/models/kubernetes" models "github.com/portainer/portainer/api/http/models/kubernetes" + "github.com/portainer/portainer/api/internal/errorlist" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -50,18 +53,20 @@ func (kcl *KubeClient) fetchServiceAccounts(namespace string) ([]models.K8sServi results := make([]models.K8sServiceAccount, 0) for _, serviceAccount := range serviceAccounts.Items { - results = append(results, parseServiceAccount(serviceAccount)) + results = append(results, kcl.parseServiceAccount(serviceAccount)) } return results, nil } // parseServiceAccount converts a corev1.ServiceAccount object to a models.K8sServiceAccount object. -func parseServiceAccount(serviceAccount corev1.ServiceAccount) models.K8sServiceAccount { +func (kcl *KubeClient) parseServiceAccount(serviceAccount corev1.ServiceAccount) models.K8sServiceAccount { return models.K8sServiceAccount{ Name: serviceAccount.Name, + UID: serviceAccount.UID, Namespace: serviceAccount.Namespace, CreationDate: serviceAccount.CreationTimestamp.Time, + IsSystem: kcl.isSystemServiceAccount(serviceAccount.Namespace), } } @@ -81,6 +86,40 @@ func (kcl *KubeClient) GetPortainerUserServiceAccount(tokenData *portainer.Token return serviceAccount, nil } +func (kcl *KubeClient) isSystemServiceAccount(namespace string) bool { + return kcl.isSystemNamespace(namespace) +} + +// DeleteServices processes a K8sServiceDeleteRequest by deleting each service +// in its given namespace. +func (kcl *KubeClient) DeleteServiceAccounts(reqs kubernetes.K8sServiceAccountDeleteRequests) error { + var errors []error + for namespace := range reqs { + for _, serviceName := range reqs[namespace] { + client := kcl.cli.CoreV1().ServiceAccounts(namespace) + + sa, err := client.Get(context.Background(), serviceName, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + continue + } + + return err + } + + if kcl.isSystemServiceAccount(sa.Namespace) { + return fmt.Errorf("cannot delete system service account %q", namespace+"/"+serviceName) + } + + if err := client.Delete(context.Background(), serviceName, metav1.DeleteOptions{}); err != nil { + errors = append(errors, err) + } + } + } + + return errorlist.Combine(errors) +} + // GetServiceAccountBearerToken returns the ServiceAccountToken associated to the specified user. func (kcl *KubeClient) GetServiceAccountBearerToken(userID int) (string, error) { serviceAccountName := UserServiceAccountName(userID, kcl.instanceID) diff --git a/api/portainer.go b/api/portainer.go index 5d07ac855..10cacf79e 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1499,6 +1499,8 @@ type ( SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error IsRBACEnabled() (bool, error) GetPortainerUserServiceAccount(tokendata *TokenData) (*corev1.ServiceAccount, error) + GetServiceAccounts(namespace string) ([]models.K8sServiceAccount, error) + DeleteServiceAccounts(reqs models.K8sServiceAccountDeleteRequests) error GetServiceAccountBearerToken(userID int) (string, error) CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error) @@ -1532,6 +1534,16 @@ type ( CreateRegistrySecret(registry *Registry, namespace string) error IsRegistrySecret(namespace, secretName string) (bool, error) ToggleSystemState(namespace string, isSystem bool) error + + GetClusterRoles() ([]models.K8sClusterRole, error) + DeleteClusterRoles(models.K8sClusterRoleDeleteRequests) error + GetClusterRoleBindings() ([]models.K8sClusterRoleBinding, error) + DeleteClusterRoleBindings(models.K8sClusterRoleBindingDeleteRequests) error + + GetRoles(namespace string) ([]models.K8sRole, error) + DeleteRoles(models.K8sRoleDeleteRequests) error + GetRoleBindings(namespace string) ([]models.K8sRoleBinding, error) + DeleteRoleBindings(models.K8sRoleBindingDeleteRequests) error } // KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes environment(endpoint) diff --git a/app/react/components/Badge/SystemBadge.tsx b/app/react/components/Badge/SystemBadge.tsx index 17552d755..e09b944ff 100644 --- a/app/react/components/Badge/SystemBadge.tsx +++ b/app/react/components/Badge/SystemBadge.tsx @@ -1,5 +1,5 @@ import { Badge } from '@@/Badge'; export function SystemBadge() { - return system; + return System; } diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/ClusterRoleBindingsDatatable.tsx b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/ClusterRoleBindingsDatatable.tsx index 1398a306b..1254fa0fa 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/ClusterRoleBindingsDatatable.tsx +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/ClusterRoleBindingsDatatable.tsx @@ -20,8 +20,8 @@ import { DefaultDatatableSettings } from '../../../datatables/DefaultDatatableSe import { ClusterRoleBinding } from './types'; import { columns } from './columns'; -import { useGetClusterRoleBindingsQuery } from './queries/useGetClusterRoleBindingsQuery'; -import { useDeleteClusterRoleBindingsMutation } from './queries/useDeleteClusterRoleBindingsMutation'; +import { useClusterRoleBindings } from './queries/useClusterRoleBindings'; +import { useDeleteClusterRoleBindings } from './queries/useDeleteClusterRoleBindings'; const storageKey = 'clusterRoleBindings'; const settingsStore = createStore(storageKey); @@ -29,12 +29,9 @@ const settingsStore = createStore(storageKey); export function ClusterRoleBindingsDatatable() { const environmentId = useEnvironmentId(); const tableState = useTableState(settingsStore, storageKey); - const clusterRoleBindingsQuery = useGetClusterRoleBindingsQuery( - environmentId, - { - autoRefreshRate: tableState.autoRefreshRate * 1000, - } - ); + const clusterRoleBindingsQuery = useClusterRoleBindings(environmentId, { + autoRefreshRate: tableState.autoRefreshRate * 1000, + }); const filteredClusterRoleBindings = useMemo( () => @@ -102,7 +99,7 @@ type TableActionsProps = { function TableActions({ selectedItems }: TableActionsProps) { const environmentId = useEnvironmentId(); const deleteClusterRoleBindingsMutation = - useDeleteClusterRoleBindingsMutation(environmentId); + useDeleteClusterRoleBindings(environmentId); const router = useRouter(); async function handleRemoveClick(roles: SelectedRole[]) { diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useGetClusterRoleBindingsQuery.ts b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useClusterRoleBindings.ts similarity index 95% rename from app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useGetClusterRoleBindingsQuery.ts rename to app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useClusterRoleBindings.ts index 3e2ce70c0..2c3d3544d 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useGetClusterRoleBindingsQuery.ts +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useClusterRoleBindings.ts @@ -9,7 +9,7 @@ import { ClusterRoleBinding } from '../types'; import { queryKeys } from './query-keys'; -export function useGetClusterRoleBindingsQuery( +export function useClusterRoleBindings( environmentId: EnvironmentId, options?: { autoRefreshRate?: number } ) { diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useDeleteClusterRoleBindingsMutation.ts b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useDeleteClusterRoleBindings.ts similarity index 91% rename from app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useDeleteClusterRoleBindingsMutation.ts rename to app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useDeleteClusterRoleBindings.ts index 9767f90e3..5724b4fb9 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useDeleteClusterRoleBindingsMutation.ts +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/queries/useDeleteClusterRoleBindings.ts @@ -6,9 +6,7 @@ import { EnvironmentId } from '@/react/portainer/environments/types'; import { queryKeys } from './query-keys'; -export function useDeleteClusterRoleBindingsMutation( - environmentId: EnvironmentId -) { +export function useDeleteClusterRoleBindings(environmentId: EnvironmentId) { const queryClient = useQueryClient(); return useMutation(deleteClusterRoleBindings, { ...withInvalidate(queryClient, [queryKeys.list(environmentId)]), diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/types.ts b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/types.ts index dcc4ff107..472e9bf82 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/types.ts +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRoleBindingsDatatable/types.ts @@ -15,12 +15,8 @@ export type ClusterRoleBinding = { name: string; uid: string; namespace: string; - resourceVersion: string; - creationDate: string; - annotations: Record | null; - roleRef: ClusterRoleRef; subjects: ClusterRoleSubject[] | null; - + creationDate: string; isSystem: boolean; }; diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/ClusterRolesDatatable.tsx b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/ClusterRolesDatatable.tsx index 58bbd3783..c61c79330 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/ClusterRolesDatatable.tsx +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/ClusterRolesDatatable.tsx @@ -15,11 +15,15 @@ import { LoadingButton } from '@@/buttons'; import { useTableState } from '@@/datatables/useTableState'; import { DefaultDatatableSettings } from '../../../datatables/DefaultDatatableSettings'; +import { useClusterRoleBindings } from '../ClusterRoleBindingsDatatable/queries/useClusterRoleBindings'; +import { useRoleBindings } from '../../RolesView/RoleBindingsDatatable/queries/useRoleBindings'; +import { ClusterRoleBinding } from '../ClusterRoleBindingsDatatable/types'; +import { RoleBinding } from '../../RolesView/RoleBindingsDatatable/types'; -import { ClusterRole } from './types'; +import { ClusterRole, ClusterRoleRowData } from './types'; import { columns } from './columns'; -import { useGetClusterRolesQuery } from './queries/useGetClusterRolesQuery'; -import { useDeleteClusterRolesMutation } from './queries/useDeleteClusterRolesMutation'; +import { useClusterRoles } from './queries/useClusterRoles'; +import { useDeleteClusterRoles } from './queries/useDeleteClusterRoles'; const storageKey = 'clusterRoles'; const settingsStore = createStore(storageKey); @@ -27,26 +31,41 @@ const settingsStore = createStore(storageKey); export function ClusterRolesDatatable() { const environmentId = useEnvironmentId(); const tableState = useTableState(settingsStore, storageKey); - const clusterRolesQuery = useGetClusterRolesQuery(environmentId, { + + const clusterRolesQuery = useClusterRoles(environmentId, { autoRefreshRate: tableState.autoRefreshRate * 1000, }); + const clusterRoleBindingsQuery = useClusterRoleBindings(environmentId); + const roleBindingsQuery = useRoleBindings(environmentId); + + const clusterRolesWithUnusedFlag = useClusterRolesWithUnusedFlag( + clusterRolesQuery.data, + clusterRoleBindingsQuery.data, + roleBindingsQuery.data + ); - const { authorized: isAuthorizedToAddEdit } = useAuthorizations([ - 'K8sClusterRolesW', - ]); const filteredClusterRoles = useMemo( () => - clusterRolesQuery.data?.filter( + clusterRolesWithUnusedFlag.filter( (cr) => tableState.showSystemResources || !cr.isSystem ), - [clusterRolesQuery.data, tableState.showSystemResources] + [clusterRolesWithUnusedFlag, tableState.showSystemResources] ); + const isLoading = + clusterRolesQuery.isLoading || + clusterRoleBindingsQuery.isLoading || + roleBindingsQuery.isLoading; + + const { authorized: isAuthorizedToAddEdit } = useAuthorizations([ + 'K8sClusterRolesW', + ]); + return ( ); } + +// Updated custom hook +function useClusterRolesWithUnusedFlag( + clusterRoles?: ClusterRole[], + clusterRoleBindings?: ClusterRoleBinding[], + roleBindings?: RoleBinding[] +): ClusterRoleRowData[] { + return useMemo(() => { + if (!clusterRoles || !clusterRoleBindings || !roleBindings) { + return []; + } + + const usedRoleNames = new Set(); + + // Check ClusterRoleBindings + clusterRoleBindings.forEach((binding) => { + if (binding.roleRef.kind === 'ClusterRole') { + usedRoleNames.add(binding.roleRef.name); + } + }); + + // Check RoleBindings + roleBindings.forEach((binding) => { + if (binding.roleRef.kind === 'ClusterRole') { + usedRoleNames.add(binding.roleRef.name); + } + }); + + // Mark cluster roles as unused if they're not in the usedRoleNames set + return clusterRoles.map((clusterRole) => ({ + ...clusterRole, + isUnused: !usedRoleNames.has(clusterRole.name) && !clusterRole.isSystem, + })); + }, [clusterRoles, clusterRoleBindings, roleBindings]); +} diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/columns/helper.ts b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/columns/helper.ts index 6dbf16107..4b22ca818 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/columns/helper.ts +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/columns/helper.ts @@ -1,5 +1,5 @@ import { createColumnHelper } from '@tanstack/react-table'; -import { ClusterRole } from '../types'; +import { ClusterRoleRowData } from '../types'; -export const columnHelper = createColumnHelper(); +export const columnHelper = createColumnHelper(); diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/columns/name.tsx b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/columns/name.tsx index b73bc6180..53addd62e 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/columns/name.tsx +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/columns/name.tsx @@ -9,9 +9,6 @@ export const name = columnHelper.accessor( if (row.isSystem) { result += ' system'; } - if (row.isUnused) { - result += ' unused'; - } return result; }, { diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useGetClusterRolesQuery.ts b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useClusterRoles.ts similarity index 96% rename from app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useGetClusterRolesQuery.ts rename to app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useClusterRoles.ts index ab221ef3a..dbacc4f4c 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useGetClusterRolesQuery.ts +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useClusterRoles.ts @@ -9,7 +9,7 @@ import { ClusterRole } from '../types'; import { queryKeys } from './query-keys'; -export function useGetClusterRolesQuery( +export function useClusterRoles( environmentId: EnvironmentId, options?: { autoRefreshRate?: number } ) { diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useDeleteClusterRolesMutation.ts b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useDeleteClusterRoles.ts similarity index 91% rename from app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useDeleteClusterRolesMutation.ts rename to app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useDeleteClusterRoles.ts index 85167e651..3aaeb8f6c 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useDeleteClusterRolesMutation.ts +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/queries/useDeleteClusterRoles.ts @@ -6,7 +6,7 @@ import { EnvironmentId } from '@/react/portainer/environments/types'; import { queryKeys } from './query-keys'; -export function useDeleteClusterRolesMutation(environmentId: EnvironmentId) { +export function useDeleteClusterRoles(environmentId: EnvironmentId) { const queryClient = useQueryClient(); return useMutation(deleteClusterRoles, { onSuccess: () => diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/types.ts b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/types.ts index ae8561a6c..b3de8ca4c 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/types.ts +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesDatatable/types.ts @@ -1,21 +1,12 @@ -export type Rule = { - verbs: string[]; - apiGroups: string[]; - resources: string[]; -}; - export type ClusterRole = { name: string; - uid: string; - namespace: string; - resourceVersion: string; creationDate: string; - annotations?: Record; - - rules: Rule[]; + uid: string; + isSystem: boolean; +}; +export type ClusterRoleRowData = ClusterRole & { isUnused: boolean; - isSystem: boolean; }; export type DeleteRequestPayload = { diff --git a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesView.tsx b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesView.tsx index 2d85ec98b..906b44e84 100644 --- a/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesView.tsx +++ b/app/react/kubernetes/more-resources/ClusterRolesView/ClusterRolesView.tsx @@ -11,7 +11,10 @@ import { ClusterRoleBindingsDatatable } from './ClusterRoleBindingsDatatable/Clu export function ClusterRolesView() { useUnauthorizedRedirect( - { authorizations: ['K8sClusterRoleBindingsW', 'K8sClusterRolesW'] }, + { + authorizations: ['K8sClusterRoleBindingsW', 'K8sClusterRolesW'], + adminOnlyCE: true, + }, { to: 'kubernetes.dashboard' } ); diff --git a/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/RoleBindingsDatatable.tsx b/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/RoleBindingsDatatable.tsx index 6083b61ef..6a70c7b68 100644 --- a/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/RoleBindingsDatatable.tsx +++ b/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/RoleBindingsDatatable.tsx @@ -2,45 +2,56 @@ import { Trash2, Link as LinkIcon } from 'lucide-react'; import { useRouter } from '@uirouter/react'; import { Row } from '@tanstack/react-table'; import clsx from 'clsx'; +import { useMemo } from 'react'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useAuthorizations, Authorized } from '@/react/hooks/useUser'; import { notifyError, notifySuccess } from '@/portainer/services/notifications'; import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription'; -import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store'; -import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; +import { + DefaultDatatableSettings, + TableSettings as KubeTableSettings, +} from '@/react/kubernetes/datatables/DefaultDatatableSettings'; import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton'; -import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace'; +import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store'; import { confirmDelete } from '@@/modals/confirm'; import { Datatable, Table, TableSettingsMenu } from '@@/datatables'; import { LoadingButton } from '@@/buttons'; -import { useTableState } from '@@/datatables/useTableState'; - -import { DefaultDatatableSettings } from '../../../datatables/DefaultDatatableSettings'; +import { + type FilteredColumnsTableSettings, + filteredColumnsSettings, +} from '@@/datatables/types'; import { RoleBinding } from './types'; import { columns } from './columns'; -import { useGetAllRoleBindingsQuery } from './queries/useGetAllRoleBindingsQuery'; -import { useDeleteRoleBindingsMutation } from './queries/useDeleteRoleBindingsMutation'; +import { useRoleBindings } from './queries/useRoleBindings'; +import { useDeleteRoleBindings } from './queries/useDeleteRoleBindings'; const storageKey = 'roleBindings'; -const settingsStore = createStore(storageKey); +interface TableSettings + extends KubeTableSettings, + FilteredColumnsTableSettings {} export function RoleBindingsDatatable() { const environmentId = useEnvironmentId(); - const tableState = useTableState(settingsStore, storageKey); - const namespacesQuery = useNamespacesQuery(environmentId); - const roleBindingsQuery = useGetAllRoleBindingsQuery(environmentId, { + const tableState = useKubeStore( + storageKey, + undefined, + (set) => ({ + ...filteredColumnsSettings(set), + }) + ); + const roleBindingsQuery = useRoleBindings(environmentId, { autoRefreshRate: tableState.autoRefreshRate * 1000, - enabled: namespacesQuery.isSuccess, }); - - const filteredRoleBindings = tableState.showSystemResources - ? roleBindingsQuery.data - : roleBindingsQuery.data?.filter( - (rb) => !isSystemNamespace(rb.namespace, namespacesQuery.data) - ); + const filteredRoleBindings = useMemo( + () => + tableState.showSystemResources + ? roleBindingsQuery.data + : roleBindingsQuery.data?.filter((rb) => !rb.isSystem), + [roleBindingsQuery.data, tableState.showSystemResources] + ); const { authorized: isAuthorisedToAddEdit } = useAuthorizations([ 'K8sRoleBindingsW', @@ -100,8 +111,7 @@ type TableActionsProps = { function TableActions({ selectedItems }: TableActionsProps) { const environmentId = useEnvironmentId(); - const deleteRoleBindingsMutation = - useDeleteRoleBindingsMutation(environmentId); + const deleteRoleBindingsMutation = useDeleteRoleBindings(environmentId); const router = useRouter(); async function handleRemoveClick(roles: SelectedRole[]) { diff --git a/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useDeleteRoleBindingsMutation.ts b/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useDeleteRoleBindings.ts similarity index 91% rename from app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useDeleteRoleBindingsMutation.ts rename to app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useDeleteRoleBindings.ts index 029ee7e4b..79c153bc5 100644 --- a/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useDeleteRoleBindingsMutation.ts +++ b/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useDeleteRoleBindings.ts @@ -6,7 +6,7 @@ import { EnvironmentId } from '@/react/portainer/environments/types'; import { queryKeys } from './query-keys'; -export function useDeleteRoleBindingsMutation(environmentId: EnvironmentId) { +export function useDeleteRoleBindings(environmentId: EnvironmentId) { const queryClient = useQueryClient(); return useMutation(deleteRoleBindings, { ...withInvalidate(queryClient, [queryKeys.list(environmentId)]), diff --git a/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useGetAllRoleBindingsQuery.ts b/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useRoleBindings.ts similarity index 95% rename from app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useGetAllRoleBindingsQuery.ts rename to app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useRoleBindings.ts index e5ddbea33..d2e399955 100644 --- a/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useGetAllRoleBindingsQuery.ts +++ b/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/queries/useRoleBindings.ts @@ -8,7 +8,7 @@ import { RoleBinding } from '../types'; import { queryKeys } from './query-keys'; -export function useGetAllRoleBindingsQuery( +export function useRoleBindings( environmentId: EnvironmentId, options?: { autoRefreshRate?: number; enabled?: boolean } ) { diff --git a/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/types.ts b/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/types.ts index 59807c588..9c2ebf737 100644 --- a/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/types.ts +++ b/app/react/kubernetes/more-resources/RolesView/RoleBindingsDatatable/types.ts @@ -15,12 +15,8 @@ export type RoleBinding = { name: string; uid: string; namespace: string; - resourceVersion: string; - creationDate: string; - annotations: Record | null; - roleRef: RoleRef; subjects: RoleSubject[] | null; - + creationDate: string; isSystem: boolean; }; diff --git a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/RolesDatatable.tsx b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/RolesDatatable.tsx index 09e183213..b24eca7f5 100644 --- a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/RolesDatatable.tsx +++ b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/RolesDatatable.tsx @@ -1,56 +1,70 @@ import { Trash2, UserCheck } from 'lucide-react'; import { useRouter } from '@uirouter/react'; +import { useMemo } from 'react'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { Authorized } from '@/react/hooks/useUser'; import { notifyError, notifySuccess } from '@/portainer/services/notifications'; import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription'; -import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store'; -import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton'; -import { useUnauthorizedRedirect } from '@/react/hooks/useUnauthorizedRedirect'; -import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace'; +import { + DefaultDatatableSettings, + TableSettings as KubeTableSettings, +} from '@/react/kubernetes/datatables/DefaultDatatableSettings'; +import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store'; import { confirmDelete } from '@@/modals/confirm'; import { Datatable, TableSettingsMenu } from '@@/datatables'; import { LoadingButton } from '@@/buttons'; -import { useTableState } from '@@/datatables/useTableState'; +import { + type FilteredColumnsTableSettings, + filteredColumnsSettings, +} from '@@/datatables/types'; -import { DefaultDatatableSettings } from '../../../datatables/DefaultDatatableSettings'; +import { useRoleBindings } from '../RoleBindingsDatatable/queries/useRoleBindings'; +import { RoleBinding } from '../RoleBindingsDatatable/types'; import { columns } from './columns'; -import { Role } from './types'; -import { useGetAllRolesQuery } from './queries/useGetAllRolesQuery'; -import { useDeleteRolesMutation } from './queries/useDeleteRolesMutation'; +import { Role, RoleRowData } from './types'; +import { useRoles } from './queries/useRoles'; +import { useDeleteRoles } from './queries/useDeleteRoles'; const storageKey = 'roles'; -const settingsStore = createStore(storageKey); +interface TableSettings + extends KubeTableSettings, + FilteredColumnsTableSettings {} export function RolesDatatable() { const environmentId = useEnvironmentId(); - const tableState = useTableState(settingsStore, storageKey); - const namespacesQuery = useNamespacesQuery(environmentId); - const rolesQuery = useGetAllRolesQuery(environmentId, { + const tableState = useKubeStore( + storageKey, + undefined, + (set) => ({ + ...filteredColumnsSettings(set), + }) + ); + const rolesQuery = useRoles(environmentId, { + autoRefreshRate: tableState.autoRefreshRate * 1000, + }); + const roleBindingsQuery = useRoleBindings(environmentId, { autoRefreshRate: tableState.autoRefreshRate * 1000, - enabled: namespacesQuery.isSuccess, }); - useUnauthorizedRedirect( - { authorizations: ['K8sRolesW'] }, - { to: 'kubernetes.dashboard' } + const roleRowData = useRoleRowData(rolesQuery.data, roleBindingsQuery.data); + + const filteredRoles = useMemo( + () => + tableState.showSystemResources + ? roleRowData + : roleRowData.filter((role) => !role.isSystem), + [roleRowData, tableState.showSystemResources] ); - const filteredRoles = tableState.showSystemResources - ? rolesQuery.data - : rolesQuery.data?.filter( - (role) => !isSystemNamespace(role.namespace, namespacesQuery.data) - ); - return ( + roles?.map((role) => { + const isUsed = roleBindings?.some( + (roleBinding) => + roleBinding.roleRef.name === role.name && + roleBinding.namespace === role.namespace + ); + return { ...role, isUnused: !isUsed }; + }), + [roles, roleBindings] + ); + + return roleRowData ?? []; +} diff --git a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/columns/helper.ts b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/columns/helper.ts index cae915061..acac7cf61 100644 --- a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/columns/helper.ts +++ b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/columns/helper.ts @@ -1,5 +1,5 @@ import { createColumnHelper } from '@tanstack/react-table'; -import { Role } from '../types'; +import { RoleRowData } from '../types'; -export const columnHelper = createColumnHelper(); +export const columnHelper = createColumnHelper(); diff --git a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/columns/namespace.tsx b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/columns/namespace.tsx index 3d2f8021d..378ef2627 100644 --- a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/columns/namespace.tsx +++ b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/columns/namespace.tsx @@ -3,7 +3,7 @@ import { Row } from '@tanstack/react-table'; import { filterHOC } from '@@/datatables/Filter'; import { Link } from '@@/Link'; -import { Role } from '../types'; +import { RoleRowData } from '../types'; import { columnHelper } from './helper'; @@ -26,7 +26,7 @@ export const namespace = columnHelper.accessor((row) => row.namespace, { filter: filterHOC('Filter by namespace'), }, enableColumnFilter: true, - filterFn: (row: Row, _columnId: string, filterValue: string[]) => + filterFn: (row: Row, _columnId: string, filterValue: string[]) => filterValue.length === 0 || filterValue.includes(row.original.namespace ?? ''), }); diff --git a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useDeleteRolesMutation.ts b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useDeleteRoles.ts similarity index 92% rename from app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useDeleteRolesMutation.ts rename to app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useDeleteRoles.ts index 211ac8a16..6b4cc96fc 100644 --- a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useDeleteRolesMutation.ts +++ b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useDeleteRoles.ts @@ -6,7 +6,7 @@ import { EnvironmentId } from '@/react/portainer/environments/types'; import { queryKeys } from './query-keys'; -export function useDeleteRolesMutation(environmentId: EnvironmentId) { +export function useDeleteRoles(environmentId: EnvironmentId) { const queryClient = useQueryClient(); return useMutation(deleteRole, { ...withInvalidate(queryClient, [queryKeys.list(environmentId)]), diff --git a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useGetAllRolesQuery.ts b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useRoles.ts similarity index 96% rename from app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useGetAllRolesQuery.ts rename to app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useRoles.ts index e7d692cd6..aa5ec8eae 100644 --- a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useGetAllRolesQuery.ts +++ b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/queries/useRoles.ts @@ -11,7 +11,7 @@ const queryKeys = { ['environments', environmentId, 'kubernetes', 'roles'] as const, }; -export function useGetAllRolesQuery( +export function useRoles( environmentId: EnvironmentId, options?: { autoRefreshRate?: number; enabled?: boolean } ) { diff --git a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/types.ts b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/types.ts index cd2bdddf6..05e30cad5 100644 --- a/app/react/kubernetes/more-resources/RolesView/RolesDatatable/types.ts +++ b/app/react/kubernetes/more-resources/RolesView/RolesDatatable/types.ts @@ -8,12 +8,10 @@ export type Role = { name: string; uid: string; namespace: string; - resourceVersion: string; creationDate: string; - annotations?: Record; - - rules: Rule[]; - isSystem: boolean; +}; + +export type RoleRowData = Role & { isUnused: boolean; }; diff --git a/app/react/kubernetes/more-resources/RolesView/RolesView.tsx b/app/react/kubernetes/more-resources/RolesView/RolesView.tsx index d849b543f..1d8394b4f 100644 --- a/app/react/kubernetes/more-resources/RolesView/RolesView.tsx +++ b/app/react/kubernetes/more-resources/RolesView/RolesView.tsx @@ -11,7 +11,7 @@ import { RoleBindingsDatatable } from './RoleBindingsDatatable'; export function RolesView() { useUnauthorizedRedirect( - { authorizations: ['K8sRoleBindingsW', 'K8sRolesW'] }, + { authorizations: ['K8sRoleBindingsW', 'K8sRolesW'], adminOnlyCE: true }, { to: 'kubernetes.dashboard' } ); diff --git a/app/react/kubernetes/more-resources/ServiceAccountsView/ServiceAccountsDatatable/ServiceAccountsDatatable.tsx b/app/react/kubernetes/more-resources/ServiceAccountsView/ServiceAccountsDatatable/ServiceAccountsDatatable.tsx index 20ddfce56..09d691be6 100644 --- a/app/react/kubernetes/more-resources/ServiceAccountsView/ServiceAccountsDatatable/ServiceAccountsDatatable.tsx +++ b/app/react/kubernetes/more-resources/ServiceAccountsView/ServiceAccountsDatatable/ServiceAccountsDatatable.tsx @@ -1,51 +1,55 @@ import { User } from 'lucide-react'; import { useRouter } from '@uirouter/react'; +import { useMemo } from 'react'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { Authorized } from '@/react/hooks/useUser'; import { notifyError, notifySuccess } from '@/portainer/services/notifications'; import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription'; -import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; -import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store'; +import { + DefaultDatatableSettings, + TableSettings as KubeTableSettings, +} from '@/react/kubernetes/datatables/DefaultDatatableSettings'; import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton'; -import { useUnauthorizedRedirect } from '@/react/hooks/useUnauthorizedRedirect'; -import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace'; +import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store'; import { Datatable, TableSettingsMenu } from '@@/datatables'; -import { useTableState } from '@@/datatables/useTableState'; import { DeleteButton } from '@@/buttons/DeleteButton'; +import { + type FilteredColumnsTableSettings, + filteredColumnsSettings, +} from '@@/datatables/types'; import { ServiceAccount } from '../types'; -import { DefaultDatatableSettings } from '../../../datatables/DefaultDatatableSettings'; -import { useColumns } from './columns'; +import { columns } from './columns'; import { useDeleteServiceAccountsMutation } from './queries/useDeleteServiceAccountsMutation'; import { useGetAllServiceAccountsQuery } from './queries/useGetAllServiceAccountsQuery'; const storageKey = 'serviceAccounts'; -const settingsStore = createStore(storageKey); +interface TableSettings + extends KubeTableSettings, + FilteredColumnsTableSettings {} export function ServiceAccountsDatatable() { - useUnauthorizedRedirect( - { authorizations: ['K8sServiceAccountsW'] }, - { to: 'kubernetes.dashboard' } - ); - const environmentId = useEnvironmentId(); - const tableState = useTableState(settingsStore, storageKey); - const namespacesQuery = useNamespacesQuery(environmentId); + const tableState = useKubeStore( + storageKey, + undefined, + (set) => ({ + ...filteredColumnsSettings(set), + }) + ); const serviceAccountsQuery = useGetAllServiceAccountsQuery(environmentId, { refetchInterval: tableState.autoRefreshRate * 1000, - enabled: namespacesQuery.isSuccess, }); - - const columns = useColumns(); - - const filteredServiceAccounts = tableState.showSystemResources - ? serviceAccountsQuery.data - : serviceAccountsQuery.data?.filter( - (sa) => !isSystemNamespace(sa.namespace, namespacesQuery.data) - ); + const filteredServiceAccounts = useMemo( + () => + tableState.showSystemResources + ? serviceAccountsQuery.data + : serviceAccountsQuery.data?.filter((sa) => !sa.isSystem), + [serviceAccountsQuery.data, tableState.showSystemResources] + ); return (
{row.original.name}
{row.original.isSystem && } - {!row.original.isSystem && row.original.isUnused && } ), } diff --git a/app/react/kubernetes/more-resources/ServiceAccountsView/ServiceAccountsView.tsx b/app/react/kubernetes/more-resources/ServiceAccountsView/ServiceAccountsView.tsx index 53e0946b3..441d45462 100644 --- a/app/react/kubernetes/more-resources/ServiceAccountsView/ServiceAccountsView.tsx +++ b/app/react/kubernetes/more-resources/ServiceAccountsView/ServiceAccountsView.tsx @@ -1,8 +1,14 @@ +import { useUnauthorizedRedirect } from '@/react/hooks/useUnauthorizedRedirect'; + import { PageHeader } from '@@/PageHeader'; import { ServiceAccountsDatatable } from './ServiceAccountsDatatable'; export function ServiceAccountsView() { + useUnauthorizedRedirect( + { authorizations: ['K8sServiceAccountsW'], adminOnlyCE: true }, + { to: 'kubernetes.dashboard' } + ); return ( <>