mirror of
https://github.com/portainer/portainer.git
synced 2025-07-22 06:49:40 +02:00
fix(kubernetes): events api to call the backend [R8S-243] (#563)
This commit is contained in:
parent
32ef208278
commit
07dfd981a2
26 changed files with 750 additions and 217 deletions
|
@ -30,8 +30,8 @@ func (handler *Handler) prepareKubeClient(r *http.Request) (*cli.KubeClient, *ht
|
||||||
log.Error().Err(err).Str("context", "prepareKubeClient").Msg("Unable to get a privileged Kubernetes client for the user.")
|
log.Error().Err(err).Str("context", "prepareKubeClient").Msg("Unable to get a privileged Kubernetes client for the user.")
|
||||||
return nil, httperror.InternalServerError("Unable to get a privileged Kubernetes client for the user.", err)
|
return nil, httperror.InternalServerError("Unable to get a privileged Kubernetes client for the user.", err)
|
||||||
}
|
}
|
||||||
pcli.IsKubeAdmin = cli.IsKubeAdmin
|
pcli.SetIsKubeAdmin(cli.GetIsKubeAdmin())
|
||||||
pcli.NonAdminNamespaces = cli.NonAdminNamespaces
|
pcli.SetClientNonAdminNamespaces(cli.GetClientNonAdminNamespaces())
|
||||||
|
|
||||||
return pcli, nil
|
return pcli, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ func (handler *Handler) getAllKubernetesClusterRoleBindings(w http.ResponseWrite
|
||||||
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", httpErr)
|
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", httpErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cli.IsKubeAdmin {
|
if !cli.GetIsKubeAdmin() {
|
||||||
log.Error().Str("context", "getAllKubernetesClusterRoleBindings").Msg("user is not authorized to fetch cluster role bindings from the Kubernetes cluster.")
|
log.Error().Str("context", "getAllKubernetesClusterRoleBindings").Msg("user is not authorized to fetch cluster role bindings from the Kubernetes cluster.")
|
||||||
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", nil)
|
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ func (handler *Handler) getAllKubernetesClusterRoles(w http.ResponseWriter, r *h
|
||||||
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", httpErr)
|
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", httpErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cli.IsKubeAdmin {
|
if !cli.GetIsKubeAdmin() {
|
||||||
log.Error().Str("context", "getAllKubernetesClusterRoles").Msg("user is not authorized to fetch cluster roles from the Kubernetes cluster.")
|
log.Error().Str("context", "getAllKubernetesClusterRoles").Msg("user is not authorized to fetch cluster roles from the Kubernetes cluster.")
|
||||||
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", nil)
|
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", nil)
|
||||||
}
|
}
|
||||||
|
|
102
api/http/handler/kubernetes/event.go
Normal file
102
api/http/handler/kubernetes/event.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id getKubernetesEventsForNamespace
|
||||||
|
// @summary Gets kubernetes events for namespace
|
||||||
|
// @description Get events by optional query param resourceId for a given namespace.
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param namespace path string true "The namespace name the events are associated to"
|
||||||
|
// @param resourceId query string false "The resource id of the involved kubernetes object" example:"e5b021b6-4bce-4c06-bd3b-6cca906797aa"
|
||||||
|
// @success 200 {object} models.Event[] "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 500 "Server error occurred while attempting to retrieve the events within the specified namespace."
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/events [get]
|
||||||
|
func (handler *Handler) getKubernetesEventsForNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesEvents").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
|
||||||
|
return httperror.BadRequest("Unable to retrieve namespace identifier route variable", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceId, err := request.RetrieveQueryParameter(r, "resourceId", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unable to retrieve resourceId query parameter")
|
||||||
|
return httperror.BadRequest("Unable to retrieve resourceId query parameter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "getKubernetesEvents").Str("resourceId", resourceId).Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := cli.GetEvents(namespace, resourceId)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unable to retrieve events")
|
||||||
|
return httperror.InternalServerError("Unable to retrieve events", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, events)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id getAllKubernetesEvents
|
||||||
|
// @summary Gets kubernetes events
|
||||||
|
// @description Get events by query param resourceId
|
||||||
|
// @description **Access policy**: Authenticated user.
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth || jwt
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Environment identifier"
|
||||||
|
// @param resourceId query string false "The resource id of the involved kubernetes object" example:"e5b021b6-4bce-4c06-bd3b-6cca906797aa"
|
||||||
|
// @success 200 {object} models.Event[] "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 500 "Server error occurred while attempting to retrieve the events."
|
||||||
|
// @router /kubernetes/{id}/events [get]
|
||||||
|
func (handler *Handler) getAllKubernetesEvents(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
resourceId, err := request.RetrieveQueryParameter(r, "resourceId", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unable to retrieve resourceId query parameter")
|
||||||
|
return httperror.BadRequest("Unable to retrieve resourceId query parameter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, httpErr := handler.getProxyKubeClient(r)
|
||||||
|
if httpErr != nil {
|
||||||
|
log.Error().Err(httpErr).Str("context", "getKubernetesEvents").Str("resourceId", resourceId).Msg("Unable to get a Kubernetes client for the user")
|
||||||
|
return httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := cli.GetEvents("", resourceId)
|
||||||
|
if err != nil {
|
||||||
|
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unauthorized access to the Kubernetes API")
|
||||||
|
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unable to retrieve events")
|
||||||
|
return httperror.InternalServerError("Unable to retrieve events", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, events)
|
||||||
|
}
|
60
api/http/handler/kubernetes/event_test.go
Normal file
60
api/http/handler/kubernetes/event_test.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
|
"github.com/portainer/portainer/api/jwt"
|
||||||
|
"github.com/portainer/portainer/api/kubernetes"
|
||||||
|
kubeClient "github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Currently this test just tests the HTTP Handler is setup correctly, in the future we should move the ClientFactory to a mock in order
|
||||||
|
// test the logic in event.go
|
||||||
|
func TestGetKubernetesEvents(t *testing.T) {
|
||||||
|
is := assert.New(t)
|
||||||
|
|
||||||
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
|
|
||||||
|
err := store.Endpoint().Create(&portainer.Endpoint{
|
||||||
|
ID: 1,
|
||||||
|
Type: portainer.AgentOnKubernetesEnvironment,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
is.NoError(err, "error creating environment")
|
||||||
|
|
||||||
|
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||||
|
is.NoError(err, "error creating a user")
|
||||||
|
|
||||||
|
jwtService, err := jwt.NewService("1h", store)
|
||||||
|
is.NoError(err, "Error initiating jwt service")
|
||||||
|
|
||||||
|
tk, _, _ := jwtService.GenerateToken(&portainer.TokenData{ID: 1, Username: "admin", Role: portainer.AdministratorRole})
|
||||||
|
|
||||||
|
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
|
||||||
|
|
||||||
|
cli := testhelpers.NewKubernetesClient()
|
||||||
|
factory, _ := kubeClient.NewClientFactory(nil, nil, store, "", "", "")
|
||||||
|
|
||||||
|
authorizationService := authorization.NewService(store)
|
||||||
|
handler := NewHandler(testhelpers.NewTestRequestBouncer(), authorizationService, store, jwtService, kubeClusterAccessService,
|
||||||
|
factory, cli)
|
||||||
|
is.NotNil(handler, "Handler should not fail")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/kubernetes/1/events?resourceId=8", nil)
|
||||||
|
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
testhelpers.AddTestSecurityCookie(req, tk)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
is.Equal(http.StatusOK, rr.Code, "Status should be 200")
|
||||||
|
}
|
|
@ -58,6 +58,7 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
||||||
endpointRouter.Handle("/configmaps/count", httperror.LoggerHandler(h.getAllKubernetesConfigMapsCount)).Methods(http.MethodGet)
|
endpointRouter.Handle("/configmaps/count", httperror.LoggerHandler(h.getAllKubernetesConfigMapsCount)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/cron_jobs", httperror.LoggerHandler(h.getAllKubernetesCronJobs)).Methods(http.MethodGet)
|
endpointRouter.Handle("/cron_jobs", httperror.LoggerHandler(h.getAllKubernetesCronJobs)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/cron_jobs/delete", httperror.LoggerHandler(h.deleteKubernetesCronJobs)).Methods(http.MethodPost)
|
endpointRouter.Handle("/cron_jobs/delete", httperror.LoggerHandler(h.deleteKubernetesCronJobs)).Methods(http.MethodPost)
|
||||||
|
endpointRouter.Handle("/events", httperror.LoggerHandler(h.getAllKubernetesEvents)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/jobs", httperror.LoggerHandler(h.getAllKubernetesJobs)).Methods(http.MethodGet)
|
endpointRouter.Handle("/jobs", httperror.LoggerHandler(h.getAllKubernetesJobs)).Methods(http.MethodGet)
|
||||||
endpointRouter.Handle("/jobs/delete", httperror.LoggerHandler(h.deleteKubernetesJobs)).Methods(http.MethodPost)
|
endpointRouter.Handle("/jobs/delete", httperror.LoggerHandler(h.deleteKubernetesJobs)).Methods(http.MethodPost)
|
||||||
endpointRouter.Handle("/cluster_roles", httperror.LoggerHandler(h.getAllKubernetesClusterRoles)).Methods(http.MethodGet)
|
endpointRouter.Handle("/cluster_roles", httperror.LoggerHandler(h.getAllKubernetesClusterRoles)).Methods(http.MethodGet)
|
||||||
|
@ -110,6 +111,7 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
||||||
// to keep it simple, we've decided to leave it like this.
|
// to keep it simple, we've decided to leave it like this.
|
||||||
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
||||||
namespaceRouter.Handle("/configmaps/{configmap}", httperror.LoggerHandler(h.getKubernetesConfigMap)).Methods(http.MethodGet)
|
namespaceRouter.Handle("/configmaps/{configmap}", httperror.LoggerHandler(h.getKubernetesConfigMap)).Methods(http.MethodGet)
|
||||||
|
namespaceRouter.Handle("/events", httperror.LoggerHandler(h.getKubernetesEventsForNamespace)).Methods(http.MethodGet)
|
||||||
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
|
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
|
||||||
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
|
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
|
||||||
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllersByNamespace)).Methods(http.MethodPut)
|
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllersByNamespace)).Methods(http.MethodPut)
|
||||||
|
@ -133,7 +135,7 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
||||||
// getProxyKubeClient gets a kubeclient for the user. It's generally what you want as it retrieves the kubeclient
|
// getProxyKubeClient gets a kubeclient for the user. It's generally what you want as it retrieves the kubeclient
|
||||||
// from the Authorization token of the currently logged in user. The kubeclient that is not from the proxy is actually using
|
// from the Authorization token of the currently logged in user. The kubeclient that is not from the proxy is actually using
|
||||||
// admin permissions. If you're unsure which one to use, use this.
|
// admin permissions. If you're unsure which one to use, use this.
|
||||||
func (h *Handler) getProxyKubeClient(r *http.Request) (*cli.KubeClient, *httperror.HandlerError) {
|
func (h *Handler) getProxyKubeClient(r *http.Request) (portainer.KubeClient, *httperror.HandlerError) {
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httperror.BadRequest(fmt.Sprintf("an error occurred during the getProxyKubeClient operation, the environment identifier route variable is invalid for /api/kubernetes/%d. Error: ", endpointID), err)
|
return nil, httperror.BadRequest(fmt.Sprintf("an error occurred during the getProxyKubeClient operation, the environment identifier route variable is invalid for /api/kubernetes/%d. Error: ", endpointID), err)
|
||||||
|
@ -253,7 +255,7 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serverURL.Scheme = "https"
|
serverURL.Scheme = "https"
|
||||||
serverURL.Host = "localhost" + handler.KubernetesClientFactory.AddrHTTPS
|
serverURL.Host = "localhost" + handler.KubernetesClientFactory.GetAddrHTTPS()
|
||||||
config.Clusters[0].Cluster.Server = serverURL.String()
|
config.Clusters[0].Cluster.Server = serverURL.String()
|
||||||
|
|
||||||
yaml, err := cli.GenerateYAML(config)
|
yaml, err := cli.GenerateYAML(config)
|
||||||
|
|
25
api/http/models/kubernetes/event.go
Normal file
25
api/http/models/kubernetes/event.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type K8sEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
EventTime time.Time `json:"eventTime"`
|
||||||
|
Kind string `json:"kind,omitempty"`
|
||||||
|
Count int32 `json:"count"`
|
||||||
|
FirstTimestamp *time.Time `json:"firstTimestamp,omitempty"`
|
||||||
|
LastTimestamp *time.Time `json:"lastTimestamp,omitempty"`
|
||||||
|
UID string `json:"uid"`
|
||||||
|
InvolvedObjectKind K8sEventInvolvedObject `json:"involvedObject"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type K8sEventInvolvedObject struct {
|
||||||
|
Kind string `json:"kind,omitempty"`
|
||||||
|
UID string `json:"uid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
}
|
19
api/internal/testhelpers/kube_client.go
Normal file
19
api/internal/testhelpers/kube_client.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package testhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testKubeClient struct {
|
||||||
|
portainer.KubeClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKubernetesClient() portainer.KubeClient {
|
||||||
|
return &testKubeClient{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event
|
||||||
|
func (kcl *testKubeClient) GetEvents(namespace string, resourceId string) ([]models.K8sEvent, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
|
@ -143,3 +143,23 @@ func (kcl *KubeClient) GetNonAdminNamespaces(userID int, teamIDs []int, isRestri
|
||||||
|
|
||||||
return nonAdminNamespaces, nil
|
return nonAdminNamespaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIsKubeAdmin retrieves true if client is admin
|
||||||
|
func (client *KubeClient) GetIsKubeAdmin() bool {
|
||||||
|
return client.IsKubeAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateIsKubeAdmin sets whether the kube client is admin
|
||||||
|
func (client *KubeClient) SetIsKubeAdmin(isKubeAdmin bool) {
|
||||||
|
client.IsKubeAdmin = isKubeAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientNonAdminNamespaces retrieves non-admin namespaces
|
||||||
|
func (client *KubeClient) GetClientNonAdminNamespaces() []string {
|
||||||
|
return client.NonAdminNamespaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateClientNonAdminNamespaces sets the client non admin namespace list
|
||||||
|
func (client *KubeClient) SetClientNonAdminNamespaces(nonAdminNamespaces []string) {
|
||||||
|
client.NonAdminNamespaces = nonAdminNamespaces
|
||||||
|
}
|
||||||
|
|
|
@ -82,6 +82,10 @@ func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID)
|
||||||
factory.endpointProxyClients.Delete(strconv.Itoa(int(endpointID)))
|
factory.endpointProxyClients.Delete(strconv.Itoa(int(endpointID)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (factory *ClientFactory) GetAddrHTTPS() string {
|
||||||
|
return factory.AddrHTTPS
|
||||||
|
}
|
||||||
|
|
||||||
// GetPrivilegedKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
|
// GetPrivilegedKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
|
||||||
// If no client is registered, it will create a new client, register it, and returns it.
|
// If no client is registered, it will create a new client, register it, and returns it.
|
||||||
func (factory *ClientFactory) GetPrivilegedKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
func (factory *ClientFactory) GetPrivilegedKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
||||||
|
|
93
api/kubernetes/cli/event.go
Normal file
93
api/kubernetes/cli/event.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetEvents gets all the Events for a given namespace and resource
|
||||||
|
// If the user is a kube admin, it returns all events in the namespace
|
||||||
|
// Otherwise, it returns only the events in the non-admin namespaces
|
||||||
|
func (kcl *KubeClient) GetEvents(namespace string, resourceId string) ([]models.K8sEvent, error) {
|
||||||
|
if kcl.IsKubeAdmin {
|
||||||
|
return kcl.fetchAllEvents(namespace, resourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kcl.fetchEventsForNonAdmin(namespace, resourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchEventsForNonAdmin returns all events in the given namespace and resource
|
||||||
|
// It returns only the events in the non-admin namespaces
|
||||||
|
func (kcl *KubeClient) fetchEventsForNonAdmin(namespace string, resourceId string) ([]models.K8sEvent, error) {
|
||||||
|
if len(kcl.NonAdminNamespaces) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := kcl.fetchAllEvents(namespace, resourceId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
||||||
|
results := make([]models.K8sEvent, 0)
|
||||||
|
for _, event := range events {
|
||||||
|
if _, ok := nonAdminNamespaceSet[event.Namespace]; ok {
|
||||||
|
results = append(results, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchEventsForNonAdmin returns all events in the given namespace and resource
|
||||||
|
// It returns all events in the namespace and resource
|
||||||
|
func (kcl *KubeClient) fetchAllEvents(namespace string, resourceId string) ([]models.K8sEvent, error) {
|
||||||
|
options := metav1.ListOptions{}
|
||||||
|
if resourceId != "" {
|
||||||
|
options.FieldSelector = "involvedObject.uid=" + resourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := kcl.cli.CoreV1().Events(namespace).List(context.TODO(), options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]models.K8sEvent, 0)
|
||||||
|
for _, event := range list.Items {
|
||||||
|
results = append(results, parseEvent(&event))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEvent(event *corev1.Event) models.K8sEvent {
|
||||||
|
result := models.K8sEvent{
|
||||||
|
Type: event.Type,
|
||||||
|
Name: event.Name,
|
||||||
|
Message: event.Message,
|
||||||
|
Reason: event.Reason,
|
||||||
|
Namespace: event.Namespace,
|
||||||
|
EventTime: event.EventTime.UTC(),
|
||||||
|
Kind: event.Kind,
|
||||||
|
Count: event.Count,
|
||||||
|
UID: string(event.ObjectMeta.GetUID()),
|
||||||
|
InvolvedObjectKind: models.K8sEventInvolvedObject{
|
||||||
|
Kind: event.InvolvedObject.Kind,
|
||||||
|
UID: string(event.InvolvedObject.UID),
|
||||||
|
Name: event.InvolvedObject.Name,
|
||||||
|
Namespace: event.InvolvedObject.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !event.LastTimestamp.Time.IsZero() {
|
||||||
|
result.LastTimestamp = &event.LastTimestamp.Time
|
||||||
|
}
|
||||||
|
if !event.FirstTimestamp.Time.IsZero() {
|
||||||
|
result.FirstTimestamp = &event.FirstTimestamp.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
108
api/kubernetes/cli/event_test.go
Normal file
108
api/kubernetes/cli/event_test.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
kfake "k8s.io/client-go/kubernetes/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGetEvents tests the GetEvents method
|
||||||
|
// It creates a fake Kubernetes client and passes it to the GetEvents method
|
||||||
|
// It then logs the fetched events and validated the data returned
|
||||||
|
func TestGetEvents(t *testing.T) {
|
||||||
|
t.Run("can get events for resource id when admin", func(t *testing.T) {
|
||||||
|
kcl := &KubeClient{
|
||||||
|
cli: kfake.NewSimpleClientset(),
|
||||||
|
instanceID: "instance",
|
||||||
|
IsKubeAdmin: true,
|
||||||
|
}
|
||||||
|
event := corev1.Event{
|
||||||
|
InvolvedObject: corev1.ObjectReference{UID: "resourceId"},
|
||||||
|
Action: "something",
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "myEvent"},
|
||||||
|
EventTime: metav1.NowMicro(),
|
||||||
|
Type: "warning",
|
||||||
|
Message: "This event has a very serious warning",
|
||||||
|
}
|
||||||
|
_, err := kcl.cli.CoreV1().Events("default").Create(context.TODO(), &event, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create Event: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := kcl.GetEvents("default", "resourceId")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to fetch Cron Jobs: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Fetched Events: %v", events)
|
||||||
|
require.Equal(t, 1, len(events), "Expected to return 1 event")
|
||||||
|
assert.Equal(t, event.Message, events[0].Message, "Expected Message to be equal to event message created")
|
||||||
|
assert.Equal(t, event.Type, events[0].Type, "Expected Type to be equal to event type created")
|
||||||
|
assert.Equal(t, event.EventTime.UTC(), events[0].EventTime, "Expected EventTime to be saved as a string from event time created")
|
||||||
|
})
|
||||||
|
t.Run("can get kubernetes events for non admin namespace when non admin", func(t *testing.T) {
|
||||||
|
kcl := &KubeClient{
|
||||||
|
cli: kfake.NewSimpleClientset(),
|
||||||
|
instanceID: "instance",
|
||||||
|
IsKubeAdmin: false,
|
||||||
|
NonAdminNamespaces: []string{"nonAdmin"},
|
||||||
|
}
|
||||||
|
event := corev1.Event{
|
||||||
|
InvolvedObject: corev1.ObjectReference{UID: "resourceId"},
|
||||||
|
Action: "something",
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: "nonAdmin", Name: "myEvent"},
|
||||||
|
EventTime: metav1.NowMicro(),
|
||||||
|
Type: "warning",
|
||||||
|
Message: "This event has a very serious warning",
|
||||||
|
}
|
||||||
|
_, err := kcl.cli.CoreV1().Events("nonAdmin").Create(context.TODO(), &event, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create Event: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := kcl.GetEvents("nonAdmin", "resourceId")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to fetch Cron Jobs: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Fetched Events: %v", events)
|
||||||
|
require.Equal(t, 1, len(events), "Expected to return 1 event")
|
||||||
|
assert.Equal(t, event.Message, events[0].Message, "Expected Message to be equal to event message created")
|
||||||
|
assert.Equal(t, event.Type, events[0].Type, "Expected Type to be equal to event type created")
|
||||||
|
assert.Equal(t, event.EventTime.UTC(), events[0].EventTime, "Expected EventTime to be saved as a string from event time created")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cannot get kubernetes events for admin namespace when non admin", func(t *testing.T) {
|
||||||
|
kcl := &KubeClient{
|
||||||
|
cli: kfake.NewSimpleClientset(),
|
||||||
|
instanceID: "instance",
|
||||||
|
IsKubeAdmin: false,
|
||||||
|
NonAdminNamespaces: []string{"nonAdmin"},
|
||||||
|
}
|
||||||
|
event := corev1.Event{
|
||||||
|
InvolvedObject: corev1.ObjectReference{UID: "resourceId"},
|
||||||
|
Action: "something",
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: "admin", Name: "myEvent"},
|
||||||
|
EventTime: metav1.NowMicro(),
|
||||||
|
Type: "warning",
|
||||||
|
Message: "This event has a very serious warning",
|
||||||
|
}
|
||||||
|
_, err := kcl.cli.CoreV1().Events("admin").Create(context.TODO(), &event, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create Event: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := kcl.GetEvents("admin", "resourceId")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to fetch Cron Jobs: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Fetched Events: %v", events)
|
||||||
|
assert.Equal(t, 0, len(events), "Expected to return 0 events")
|
||||||
|
})
|
||||||
|
}
|
147
api/portainer.go
147
api/portainer.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
"github.com/portainer/portainer/pkg/featureflags"
|
"github.com/portainer/portainer/pkg/featureflags"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/segmentio/encoding/json"
|
"github.com/segmentio/encoding/json"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
@ -1531,56 +1533,127 @@ type (
|
||||||
|
|
||||||
// KubeClient represents a service used to query a Kubernetes environment(endpoint)
|
// KubeClient represents a service used to query a Kubernetes environment(endpoint)
|
||||||
KubeClient interface {
|
KubeClient interface {
|
||||||
ServerVersion() (*version.Info, error)
|
// Access
|
||||||
|
GetIsKubeAdmin() bool
|
||||||
|
SetIsKubeAdmin(isKubeAdmin bool)
|
||||||
|
GetClientNonAdminNamespaces() []string
|
||||||
|
SetClientNonAdminNamespaces([]string)
|
||||||
|
NamespaceAccessPoliciesDeleteNamespace(ns string) error
|
||||||
|
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
|
||||||
|
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
||||||
|
GetNonAdminNamespaces(userID int, teamIDs []int, isRestrictDefaultNamespace bool) ([]string, error)
|
||||||
|
|
||||||
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
// Applications
|
||||||
IsRBACEnabled() (bool, error)
|
GetApplications(namespace, nodeName string) ([]models.K8sApplication, error)
|
||||||
GetPortainerUserServiceAccount(tokendata *TokenData) (*corev1.ServiceAccount, error)
|
GetApplicationsResource(namespace, node string) (models.K8sApplicationResource, error)
|
||||||
GetServiceAccounts(namespace string) ([]models.K8sServiceAccount, error)
|
|
||||||
DeleteServiceAccounts(reqs models.K8sServiceAccountDeleteRequests) error
|
// ClusterRole
|
||||||
GetServiceAccountBearerToken(userID int) (string, error)
|
GetClusterRoles() ([]models.K8sClusterRole, error)
|
||||||
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
|
DeleteClusterRoles(req models.K8sClusterRoleDeleteRequests) error
|
||||||
|
|
||||||
|
// ConfigMap
|
||||||
|
GetConfigMap(namespace, configMapName string) (models.K8sConfigMap, error)
|
||||||
|
CombineConfigMapWithApplications(configMap models.K8sConfigMap) (models.K8sConfigMap, error)
|
||||||
|
|
||||||
|
// CronJob
|
||||||
|
GetCronJobs(namespace string) ([]models.K8sCronJob, error)
|
||||||
|
DeleteCronJobs(payload models.K8sCronJobDeleteRequests) error
|
||||||
|
|
||||||
|
// Event
|
||||||
|
GetEvents(namespace string, resourceId string) ([]models.K8sEvent, error)
|
||||||
|
|
||||||
|
// Exec
|
||||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
|
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
|
||||||
|
|
||||||
|
// ClusterRoleBinding
|
||||||
|
GetClusterRoleBindings() ([]models.K8sClusterRoleBinding, error)
|
||||||
|
DeleteClusterRoleBindings(reqs models.K8sClusterRoleBindingDeleteRequests) error
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
GetDashboard() (models.K8sDashboard, error)
|
||||||
|
|
||||||
|
// Deployment
|
||||||
HasStackName(namespace string, stackName string) (bool, error)
|
HasStackName(namespace string, stackName string) (bool, error)
|
||||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
|
||||||
CreateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error)
|
// Ingress
|
||||||
UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error)
|
|
||||||
GetNamespaces() (map[string]K8sNamespaceInfo, error)
|
|
||||||
GetNamespace(string) (K8sNamespaceInfo, error)
|
|
||||||
DeleteNamespace(namespace string) (*corev1.Namespace, error)
|
|
||||||
GetConfigMaps(namespace string) ([]models.K8sConfigMap, error)
|
|
||||||
GetSecrets(namespace string) ([]models.K8sSecret, error)
|
|
||||||
GetIngressControllers() (models.K8sIngressControllers, error)
|
GetIngressControllers() (models.K8sIngressControllers, error)
|
||||||
GetApplications(namespace, nodename string) ([]models.K8sApplication, error)
|
GetIngress(namespace, ingressName string) (models.K8sIngressInfo, error)
|
||||||
GetMetrics() (models.K8sMetrics, error)
|
|
||||||
GetStorage() ([]KubernetesStorageClassConfig, error)
|
|
||||||
CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error
|
|
||||||
UpdateIngress(namespace string, info models.K8sIngressInfo) error
|
|
||||||
GetIngresses(namespace string) ([]models.K8sIngressInfo, error)
|
GetIngresses(namespace string) ([]models.K8sIngressInfo, error)
|
||||||
|
CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error
|
||||||
DeleteIngresses(reqs models.K8sIngressDeleteRequests) error
|
DeleteIngresses(reqs models.K8sIngressDeleteRequests) error
|
||||||
CreateService(namespace string, service models.K8sServiceInfo) error
|
UpdateIngress(namespace string, info models.K8sIngressInfo) error
|
||||||
UpdateService(namespace string, service models.K8sServiceInfo) error
|
CombineIngressWithService(ingress models.K8sIngressInfo) (models.K8sIngressInfo, error)
|
||||||
GetServices(namespace string) ([]models.K8sServiceInfo, error)
|
CombineIngressesWithServices(ingresses []models.K8sIngressInfo) ([]models.K8sIngressInfo, error)
|
||||||
DeleteServices(reqs models.K8sServiceDeleteRequests) error
|
|
||||||
|
// Job
|
||||||
|
GetJobs(namespace string, includeCronJobChildren bool) ([]models.K8sJob, error)
|
||||||
|
DeleteJobs(payload models.K8sJobDeleteRequests) error
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
GetMetrics() (models.K8sMetrics, error)
|
||||||
|
|
||||||
|
// Namespace
|
||||||
|
ToggleSystemState(namespaceName string, isSystem bool) error
|
||||||
|
UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error)
|
||||||
|
GetNamespace(name string) (K8sNamespaceInfo, error)
|
||||||
|
CreateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error)
|
||||||
|
GetNamespaces() (map[string]K8sNamespaceInfo, error)
|
||||||
|
CombineNamespaceWithResourceQuota(namespace K8sNamespaceInfo, w http.ResponseWriter) *httperror.HandlerError
|
||||||
|
DeleteNamespace(namespaceName string) (*corev1.Namespace, error)
|
||||||
|
CombineNamespacesWithResourceQuotas(namespaces map[string]K8sNamespaceInfo, w http.ResponseWriter) *httperror.HandlerError
|
||||||
|
ConvertNamespaceMapToSlice(namespaces map[string]K8sNamespaceInfo) []K8sNamespaceInfo
|
||||||
|
|
||||||
|
// NodeLimits
|
||||||
GetNodesLimits() (K8sNodesLimits, error)
|
GetNodesLimits() (K8sNodesLimits, error)
|
||||||
GetMaxResourceLimits(name string, overCommitEnabled bool, resourceOverCommitPercent int) (K8sNodeLimits, error)
|
GetMaxResourceLimits(skipNamespace string, overCommitEnabled bool, resourceOverCommitPercent int) (K8sNodeLimits, error)
|
||||||
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
|
||||||
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
|
// Pod
|
||||||
|
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
|
||||||
|
|
||||||
|
// RBAC
|
||||||
|
IsRBACEnabled() (bool, error)
|
||||||
|
|
||||||
|
// Registries
|
||||||
DeleteRegistrySecret(registry RegistryID, namespace string) error
|
DeleteRegistrySecret(registry RegistryID, namespace string) error
|
||||||
CreateRegistrySecret(registry *Registry, namespace string) error
|
CreateRegistrySecret(registry *Registry, namespace string) error
|
||||||
IsRegistrySecret(namespace, secretName string) (bool, error)
|
IsRegistrySecret(namespace, secretName string) (bool, error)
|
||||||
ToggleSystemState(namespace string, isSystem bool) error
|
|
||||||
|
|
||||||
GetClusterRoles() ([]models.K8sClusterRole, error)
|
// RoleBinding
|
||||||
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)
|
GetRoleBindings(namespace string) ([]models.K8sRoleBinding, error)
|
||||||
DeleteRoleBindings(models.K8sRoleBindingDeleteRequests) error
|
DeleteRoleBindings(reqs models.K8sRoleBindingDeleteRequests) error
|
||||||
|
|
||||||
|
// Role
|
||||||
|
DeleteRoles(reqs models.K8sRoleDeleteRequests) error
|
||||||
|
|
||||||
|
// Secret
|
||||||
|
GetSecrets(namespace string) ([]models.K8sSecret, error)
|
||||||
|
GetSecret(namespace string, secretName string) (models.K8sSecret, error)
|
||||||
|
CombineSecretWithApplications(secret models.K8sSecret) (models.K8sSecret, error)
|
||||||
|
|
||||||
|
// ServiceAccount
|
||||||
|
GetServiceAccounts(namespace string) ([]models.K8sServiceAccount, error)
|
||||||
|
DeleteServiceAccounts(reqs models.K8sServiceAccountDeleteRequests) error
|
||||||
|
SetupUserServiceAccount(int, []int, bool) error
|
||||||
|
GetPortainerUserServiceAccount(tokendata *TokenData) (*corev1.ServiceAccount, error)
|
||||||
|
GetServiceAccountBearerToken(userID int) (string, error)
|
||||||
|
|
||||||
|
// Service
|
||||||
|
GetServices(namespace string) ([]models.K8sServiceInfo, error)
|
||||||
|
CombineServicesWithApplications(services []models.K8sServiceInfo) ([]models.K8sServiceInfo, error)
|
||||||
|
CreateService(namespace string, info models.K8sServiceInfo) error
|
||||||
|
DeleteServices(reqs models.K8sServiceDeleteRequests) error
|
||||||
|
UpdateService(namespace string, info models.K8sServiceInfo) error
|
||||||
|
|
||||||
|
// ServerVersion
|
||||||
|
ServerVersion() (*version.Info, error)
|
||||||
|
|
||||||
|
// Storage
|
||||||
|
GetStorage() ([]KubernetesStorageClassConfig, error)
|
||||||
|
|
||||||
|
// Volumes
|
||||||
|
GetVolumes(namespace string) ([]models.K8sVolumeInfo, error)
|
||||||
|
GetVolume(namespace, volumeName string) (*models.K8sVolumeInfo, error)
|
||||||
|
CombineVolumesWithApplications(volumes *[]models.K8sVolumeInfo) (*[]models.K8sVolumeInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes environment(endpoint)
|
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes environment(endpoint)
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
<resource-events-datatable
|
<resource-events-datatable
|
||||||
resource-id="ctrl.configuration.Id"
|
resource-id="ctrl.configuration.Id"
|
||||||
storage-key="'kubernetes.configmap.events'"
|
storage-key="'kubernetes.configmap.events'"
|
||||||
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
|
namespace="ctrl.configuration.Namespace"
|
||||||
></resource-events-datatable>
|
></resource-events-datatable>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">
|
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
<resource-events-datatable
|
<resource-events-datatable
|
||||||
resource-id="ctrl.configuration.Id"
|
resource-id="ctrl.configuration.Id"
|
||||||
storage-key="'kubernetes.secret.events'"
|
storage-key="'kubernetes.secret.events'"
|
||||||
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
|
namespace="ctrl.configuration.Namespace"
|
||||||
></resource-events-datatable>
|
></resource-events-datatable>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">
|
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||||
|
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||||
|
import { UserViewModel } from '@/portainer/models/user';
|
||||||
|
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||||
|
import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||||
|
|
||||||
|
import { TableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
|
import { Event } from '../../queries/types';
|
||||||
|
|
||||||
|
import { EventsDatatable } from './EventsDatatable';
|
||||||
|
|
||||||
|
// Mock the necessary hooks and dependencies
|
||||||
|
const mockTableState: TableState<TableSettings> = {
|
||||||
|
sortBy: { id: 'Date', desc: true },
|
||||||
|
pageSize: 10,
|
||||||
|
search: '',
|
||||||
|
autoRefreshRate: 0,
|
||||||
|
showSystemResources: false,
|
||||||
|
setSortBy: vi.fn(),
|
||||||
|
setPageSize: vi.fn(),
|
||||||
|
setSearch: vi.fn(),
|
||||||
|
setAutoRefreshRate: vi.fn(),
|
||||||
|
setShowSystemResources: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('../../datatables/default-kube-datatable-store', () => ({
|
||||||
|
useKubeStore: () => mockTableState,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function renderComponent() {
|
||||||
|
const user = new UserViewModel({ Username: 'user' });
|
||||||
|
|
||||||
|
const events: Event[] = [
|
||||||
|
{
|
||||||
|
type: 'Warning',
|
||||||
|
name: 'name',
|
||||||
|
message: 'not sure if this what you want to do',
|
||||||
|
namespace: 'default',
|
||||||
|
reason: 'unknown',
|
||||||
|
count: 1,
|
||||||
|
eventTime: new Date('2025-01-02T15:04:05Z'),
|
||||||
|
uid: '4500fc9c-0cc8-4695-b4c4-989ac021d1d6',
|
||||||
|
involvedObject: {
|
||||||
|
kind: 'configMap',
|
||||||
|
uid: '35',
|
||||||
|
name: 'name',
|
||||||
|
namespace: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Wrapped = withTestQueryProvider(
|
||||||
|
withUserProvider(
|
||||||
|
withTestRouter(() => (
|
||||||
|
<EventsDatatable
|
||||||
|
dataset={events}
|
||||||
|
tableState={mockTableState}
|
||||||
|
isLoading={false}
|
||||||
|
data-cy="k8sNodeDetail-eventsTable"
|
||||||
|
noWidget
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
user
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return { ...render(<Wrapped />), events };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('EventsDatatable', () => {
|
||||||
|
it('should display events when data is loaded', async () => {
|
||||||
|
const { events } = renderComponent();
|
||||||
|
const event = events[0];
|
||||||
|
|
||||||
|
expect(screen.getByText(event.message || '')).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText(event.type || '')).toHaveLength(2);
|
||||||
|
expect(screen.getAllByText(event.involvedObject.kind || '')).toHaveLength(
|
||||||
|
2
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
import { Event } from 'kubernetes-types/core/v1';
|
|
||||||
import { History } from 'lucide-react';
|
import { History } from 'lucide-react';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { Event } from '@/react/kubernetes/queries/types';
|
||||||
import { IndexOptional } from '@/react/kubernetes/configs/types';
|
import { IndexOptional } from '@/react/kubernetes/configs/types';
|
||||||
import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ export function EventsDatatable({
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title={title}
|
title={title}
|
||||||
titleIcon={titleIcon}
|
titleIcon={titleIcon}
|
||||||
getRowId={(row) => row.metadata?.uid || ''}
|
getRowId={(row) => row.uid || ''}
|
||||||
disableSelect
|
disableSelect
|
||||||
renderTableSettings={() => (
|
renderTableSettings={() => (
|
||||||
<TableSettingsMenu>
|
<TableSettingsMenu>
|
||||||
|
|
|
@ -29,9 +29,7 @@ export function ResourceEventsDatatable({
|
||||||
params: { endpointId },
|
params: { endpointId },
|
||||||
} = useCurrentStateAndParams();
|
} = useCurrentStateAndParams();
|
||||||
|
|
||||||
const params = resourceId
|
const params = resourceId ? { resourceId: `${resourceId}` } : {};
|
||||||
? { fieldSelector: `involvedObject.uid=${resourceId}` }
|
|
||||||
: {};
|
|
||||||
const resourceEventsQuery = useEvents(endpointId, {
|
const resourceEventsQuery = useEvents(endpointId, {
|
||||||
namespace,
|
namespace,
|
||||||
params,
|
params,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Row } from '@tanstack/react-table';
|
import { Row } from '@tanstack/react-table';
|
||||||
import { Event } from 'kubernetes-types/core/v1';
|
|
||||||
|
import { Event } from '@/react/kubernetes/queries/types';
|
||||||
|
|
||||||
import { Badge, BadgeType } from '@@/Badge';
|
import { Badge, BadgeType } from '@@/Badge';
|
||||||
import { filterHOC } from '@@/datatables/Filter';
|
import { filterHOC } from '@@/datatables/Filter';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createColumnHelper } from '@tanstack/react-table';
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
import { Event } from 'kubernetes-types/core/v1';
|
|
||||||
|
import { Event } from '@/react/kubernetes/queries/types';
|
||||||
|
|
||||||
export const columnHelper = createColumnHelper<Event>();
|
export const columnHelper = createColumnHelper<Event>();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Row } from '@tanstack/react-table';
|
import { Row } from '@tanstack/react-table';
|
||||||
import { Event } from 'kubernetes-types/core/v1';
|
|
||||||
|
import { Event } from '@/react/kubernetes/queries/types';
|
||||||
|
|
||||||
import { filterHOC } from '@@/datatables/Filter';
|
import { filterHOC } from '@@/datatables/Filter';
|
||||||
|
|
||||||
|
|
|
@ -184,15 +184,8 @@ describe(
|
||||||
http.get('/api/endpoints/3/kubernetes/helm/test-release/history', () =>
|
http.get('/api/endpoints/3/kubernetes/helm/test-release/history', () =>
|
||||||
HttpResponse.json(helmReleaseHistory)
|
HttpResponse.json(helmReleaseHistory)
|
||||||
),
|
),
|
||||||
http.get(
|
http.get('/api/kubernetes/3/namespaces/default/events', () =>
|
||||||
'/api/endpoints/3/kubernetes/api/v1/namespaces/default/events',
|
HttpResponse.json([])
|
||||||
() =>
|
|
||||||
HttpResponse.json({
|
|
||||||
kind: 'EventList',
|
|
||||||
apiVersion: 'v1',
|
|
||||||
metadata: { resourceVersion: '12345' },
|
|
||||||
items: [],
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -236,15 +229,8 @@ describe(
|
||||||
HttpResponse.error()
|
HttpResponse.error()
|
||||||
),
|
),
|
||||||
// Add mock for events endpoint
|
// Add mock for events endpoint
|
||||||
http.get(
|
http.get('/api/kubernetes/3/namespaces/default/events', () =>
|
||||||
'/api/endpoints/3/kubernetes/api/v1/namespaces/default/events',
|
HttpResponse.json([])
|
||||||
() =>
|
|
||||||
HttpResponse.json({
|
|
||||||
kind: 'EventList',
|
|
||||||
apiVersion: 'v1',
|
|
||||||
metadata: { resourceVersion: '12345' },
|
|
||||||
items: [],
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -274,15 +260,8 @@ describe(
|
||||||
http.get('/api/endpoints/3/kubernetes/helm/test-release/history', () =>
|
http.get('/api/endpoints/3/kubernetes/helm/test-release/history', () =>
|
||||||
HttpResponse.json(helmReleaseHistory)
|
HttpResponse.json(helmReleaseHistory)
|
||||||
),
|
),
|
||||||
http.get(
|
http.get('/api/kubernetes/3/namespaces/default/events', () =>
|
||||||
'/api/endpoints/3/kubernetes/api/v1/namespaces/default/events',
|
HttpResponse.json([])
|
||||||
() =>
|
|
||||||
HttpResponse.json({
|
|
||||||
kind: 'EventList',
|
|
||||||
apiVersion: 'v1',
|
|
||||||
metadata: { resourceVersion: '12345' },
|
|
||||||
items: [],
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import { HttpResponse } from 'msw';
|
import { HttpResponse } from 'msw';
|
||||||
import { Event, EventList } from 'kubernetes-types/core/v1';
|
|
||||||
|
|
||||||
|
import { Event } from '@/react/kubernetes/queries/types';
|
||||||
import { server, http } from '@/setup-tests/server';
|
import { server, http } from '@/setup-tests/server';
|
||||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||||
import { withTestRouter } from '@/react/test-utils/withRouter';
|
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||||
|
@ -56,136 +56,84 @@ const testResources: GenericResource[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockEventsResponse: EventList = {
|
const mockEventsResponse: Event[] = [
|
||||||
kind: 'EventList',
|
|
||||||
apiVersion: 'v1',
|
|
||||||
metadata: {
|
|
||||||
resourceVersion: '12345',
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
metadata: {
|
|
||||||
name: 'test-deployment-123456',
|
name: 'test-deployment-123456',
|
||||||
namespace: 'default',
|
namespace: 'default',
|
||||||
|
reason: 'CreatedLoadBalancer',
|
||||||
|
eventTime: new Date('2023-01-01T00:00:00Z'),
|
||||||
uid: 'event-uid-1',
|
uid: 'event-uid-1',
|
||||||
resourceVersion: '1000',
|
|
||||||
creationTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
involvedObject: {
|
involvedObject: {
|
||||||
kind: 'Deployment',
|
kind: 'Deployment',
|
||||||
namespace: 'default',
|
|
||||||
name: 'test-deployment',
|
name: 'test-deployment',
|
||||||
uid: 'test-deployment-uid',
|
uid: 'test-deployment-uid',
|
||||||
apiVersion: 'apps/v1',
|
namespace: 'default',
|
||||||
resourceVersion: '2000',
|
|
||||||
},
|
},
|
||||||
reason: 'ScalingReplicaSet',
|
|
||||||
message: 'Scaled up replica set test-deployment-abc123 to 1',
|
message: 'Scaled up replica set test-deployment-abc123 to 1',
|
||||||
source: {
|
firstTimestamp: new Date('2023-01-01T00:00:00Z'),
|
||||||
component: 'deployment-controller',
|
lastTimestamp: new Date('2023-01-01T00:00:00Z'),
|
||||||
},
|
|
||||||
firstTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
lastTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
count: 1,
|
count: 1,
|
||||||
type: 'Normal',
|
type: 'Normal',
|
||||||
reportingComponent: 'deployment-controller',
|
|
||||||
reportingInstance: '',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
metadata: {
|
|
||||||
name: 'test-service-123456',
|
name: 'test-service-123456',
|
||||||
namespace: 'default',
|
namespace: 'default',
|
||||||
uid: 'event-uid-2',
|
uid: 'event-uid-2',
|
||||||
resourceVersion: '1001',
|
eventTime: new Date('2023-01-01T00:00:00Z'),
|
||||||
creationTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
involvedObject: {
|
involvedObject: {
|
||||||
kind: 'Service',
|
kind: 'Service',
|
||||||
namespace: 'default',
|
namespace: 'default',
|
||||||
name: 'test-service',
|
name: 'test-service',
|
||||||
uid: 'test-service-uid',
|
uid: 'test-service-uid',
|
||||||
apiVersion: 'v1',
|
|
||||||
resourceVersion: '2001',
|
|
||||||
},
|
},
|
||||||
reason: 'CreatedLoadBalancer',
|
reason: 'CreatedLoadBalancer',
|
||||||
message: 'Created load balancer',
|
message: 'Created load balancer',
|
||||||
source: {
|
firstTimestamp: new Date('2023-01-01T00:00:00Z'),
|
||||||
component: 'service-controller',
|
lastTimestamp: new Date('2023-01-01T00:00:00Z'),
|
||||||
},
|
|
||||||
firstTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
lastTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
count: 1,
|
count: 1,
|
||||||
type: 'Normal',
|
type: 'Normal',
|
||||||
reportingComponent: 'service-controller',
|
|
||||||
reportingInstance: '',
|
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
};
|
|
||||||
|
|
||||||
const mixedEventsResponse: EventList = {
|
const mixedEventsResponse: Event[] = [
|
||||||
kind: 'EventList',
|
|
||||||
apiVersion: 'v1',
|
|
||||||
metadata: {
|
|
||||||
resourceVersion: '12345',
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
metadata: {
|
|
||||||
name: 'test-deployment-123456',
|
name: 'test-deployment-123456',
|
||||||
namespace: 'default',
|
namespace: 'default',
|
||||||
uid: 'event-uid-1',
|
uid: 'event-uid-1',
|
||||||
resourceVersion: '1000',
|
eventTime: new Date('2023-01-01T00:00:00Z'),
|
||||||
creationTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
involvedObject: {
|
involvedObject: {
|
||||||
kind: 'Deployment',
|
kind: 'Deployment',
|
||||||
namespace: 'default',
|
namespace: 'default',
|
||||||
name: 'test-deployment',
|
name: 'test-deployment',
|
||||||
uid: 'test-deployment-uid', // This matches a resource UID
|
uid: 'test-deployment-uid', // This matches a resource UID
|
||||||
apiVersion: 'apps/v1',
|
|
||||||
resourceVersion: '2000',
|
|
||||||
},
|
},
|
||||||
reason: 'ScalingReplicaSet',
|
reason: 'ScalingReplicaSet',
|
||||||
message: 'Scaled up replica set test-deployment-abc123 to 1',
|
message: 'Scaled up replica set test-deployment-abc123 to 1',
|
||||||
source: {
|
|
||||||
component: 'deployment-controller',
|
firstTimestamp: new Date('2023-01-01T00:00:00Z'),
|
||||||
},
|
lastTimestamp: new Date('2023-01-01T00:00:00Z'),
|
||||||
firstTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
lastTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
count: 1,
|
count: 1,
|
||||||
type: 'Normal',
|
type: 'Normal',
|
||||||
reportingComponent: 'deployment-controller',
|
|
||||||
reportingInstance: '',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
metadata: {
|
|
||||||
name: 'unrelated-pod-123456',
|
name: 'unrelated-pod-123456',
|
||||||
namespace: 'default',
|
namespace: 'default',
|
||||||
uid: 'event-uid-3',
|
uid: 'event-uid-3',
|
||||||
resourceVersion: '1002',
|
eventTime: new Date('2023-01-01T00:00:00Z'),
|
||||||
creationTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
involvedObject: {
|
involvedObject: {
|
||||||
kind: 'Pod',
|
kind: 'Pod',
|
||||||
namespace: 'default',
|
namespace: 'default',
|
||||||
name: 'unrelated-pod',
|
name: 'unrelated-pod',
|
||||||
uid: 'unrelated-pod-uid', // This does NOT match any resource UIDs
|
uid: 'unrelated-pod-uid', // This does NOT match any resource UIDs
|
||||||
apiVersion: 'v1',
|
|
||||||
resourceVersion: '2002',
|
|
||||||
},
|
},
|
||||||
reason: 'Scheduled',
|
reason: 'Scheduled',
|
||||||
message: 'Successfully assigned unrelated-pod to node',
|
message: 'Successfully assigned unrelated-pod to node',
|
||||||
source: {
|
type: 'Normal',
|
||||||
component: 'default-scheduler',
|
firstTimestamp: new Date('2023-01-01T00:00:00Z'),
|
||||||
},
|
lastTimestamp: new Date('2023-01-01T00:00:00Z'),
|
||||||
firstTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
lastTimestamp: '2023-01-01T00:00:00Z',
|
|
||||||
count: 1,
|
count: 1,
|
||||||
reportingComponent: 'scheduler',
|
|
||||||
reportingInstance: '',
|
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
};
|
|
||||||
|
|
||||||
function renderComponent() {
|
function renderComponent() {
|
||||||
const user = new UserViewModel({ Username: 'user' });
|
const user = new UserViewModel({ Username: 'user' });
|
||||||
|
@ -229,7 +177,7 @@ describe('HelmEventsDatatable', () => {
|
||||||
|
|
||||||
it('should correctly filter related events using the filterRelatedEvents function', () => {
|
it('should correctly filter related events using the filterRelatedEvents function', () => {
|
||||||
const filteredEvents = filterRelatedEvents(
|
const filteredEvents = filterRelatedEvents(
|
||||||
mixedEventsResponse.items as Event[],
|
mixedEventsResponse as Event[],
|
||||||
testResources
|
testResources
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { compact } from 'lodash';
|
import { compact } from 'lodash';
|
||||||
import { Event } from 'kubernetes-types/core/v1';
|
|
||||||
|
|
||||||
|
import { Event } from '@/react/kubernetes/queries/types';
|
||||||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||||
import { EventsDatatable } from '@/react/kubernetes/components/EventsDatatable';
|
import { EventsDatatable } from '@/react/kubernetes/components/EventsDatatable';
|
||||||
import { useEvents } from '@/react/kubernetes/queries/useEvents';
|
import { useEvents } from '@/react/kubernetes/queries/useEvents';
|
||||||
|
|
19
app/react/kubernetes/queries/types.ts
Normal file
19
app/react/kubernetes/queries/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export type Event = {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
reason: string;
|
||||||
|
message: string;
|
||||||
|
namespace: string;
|
||||||
|
eventTime: Date;
|
||||||
|
kind?: string;
|
||||||
|
count: number;
|
||||||
|
lastTimestamp?: Date;
|
||||||
|
firstTimestamp?: Date;
|
||||||
|
uid: string;
|
||||||
|
involvedObject: {
|
||||||
|
uid: string;
|
||||||
|
kind?: string;
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import { EventList, Event } from 'kubernetes-types/core/v1';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { Event } from '@/react/kubernetes/queries/types';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import axios from '@/portainer/services/axios';
|
import axios from '@/portainer/services/axios';
|
||||||
import { withGlobalError } from '@/react-tools/react-query';
|
import { withGlobalError } from '@/react-tools/react-query';
|
||||||
|
@ -13,10 +13,7 @@ type RequestOptions = {
|
||||||
/** if undefined, events are fetched at the cluster scope */
|
/** if undefined, events are fetched at the cluster scope */
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
params?: {
|
params?: {
|
||||||
/** https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors */
|
resourceId?: string;
|
||||||
labelSelector?: string;
|
|
||||||
/** https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors */
|
|
||||||
fieldSelector?: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -44,13 +41,13 @@ async function getEvents(
|
||||||
): Promise<Event[]> {
|
): Promise<Event[]> {
|
||||||
const { namespace, params } = options ?? {};
|
const { namespace, params } = options ?? {};
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<EventList>(
|
const { data } = await axios.get<Event[]>(
|
||||||
buildUrl(environmentId, namespace),
|
buildUrl(environmentId, namespace),
|
||||||
{
|
{
|
||||||
params,
|
params,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data.items;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw parseKubernetesAxiosError(e, 'Unable to retrieve events');
|
throw parseKubernetesAxiosError(e, 'Unable to retrieve events');
|
||||||
}
|
}
|
||||||
|
@ -96,6 +93,6 @@ export function useEventWarningsCount(
|
||||||
|
|
||||||
function buildUrl(environmentId: EnvironmentId, namespace?: string) {
|
function buildUrl(environmentId: EnvironmentId, namespace?: string) {
|
||||||
return namespace
|
return namespace
|
||||||
? `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/events`
|
? `/kubernetes/${environmentId}/namespaces/${namespace}/events`
|
||||||
: `/endpoints/${environmentId}/kubernetes/api/v1/events`;
|
: `/kubernetes/${environmentId}/events`;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue