diff --git a/api/http/handler/kubernetes/client.go b/api/http/handler/kubernetes/client.go
index a85f2cff9..6612ab7f4 100644
--- a/api/http/handler/kubernetes/client.go
+++ b/api/http/handler/kubernetes/client.go
@@ -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.")
return nil, httperror.InternalServerError("Unable to get a privileged Kubernetes client for the user.", err)
}
- pcli.IsKubeAdmin = cli.IsKubeAdmin
- pcli.NonAdminNamespaces = cli.NonAdminNamespaces
+ pcli.SetIsKubeAdmin(cli.GetIsKubeAdmin())
+ pcli.SetClientNonAdminNamespaces(cli.GetClientNonAdminNamespaces())
return pcli, nil
}
diff --git a/api/http/handler/kubernetes/cluster_role_bindings.go b/api/http/handler/kubernetes/cluster_role_bindings.go
index a5050c947..83621a900 100644
--- a/api/http/handler/kubernetes/cluster_role_bindings.go
+++ b/api/http/handler/kubernetes/cluster_role_bindings.go
@@ -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)
}
- 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.")
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", nil)
}
diff --git a/api/http/handler/kubernetes/cluster_roles.go b/api/http/handler/kubernetes/cluster_roles.go
index 3fd2ca8aa..6d5d028be 100644
--- a/api/http/handler/kubernetes/cluster_roles.go
+++ b/api/http/handler/kubernetes/cluster_roles.go
@@ -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)
}
- if !cli.IsKubeAdmin {
+ if !cli.GetIsKubeAdmin() {
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)
}
diff --git a/api/http/handler/kubernetes/event.go b/api/http/handler/kubernetes/event.go
new file mode 100644
index 000000000..0e226d5ec
--- /dev/null
+++ b/api/http/handler/kubernetes/event.go
@@ -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)
+}
diff --git a/api/http/handler/kubernetes/event_test.go b/api/http/handler/kubernetes/event_test.go
new file mode 100644
index 000000000..77f38c511
--- /dev/null
+++ b/api/http/handler/kubernetes/event_test.go
@@ -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")
+}
diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go
index cc068e4a4..07f6bdf3f 100644
--- a/api/http/handler/kubernetes/handler.go
+++ b/api/http/handler/kubernetes/handler.go
@@ -58,6 +58,7 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
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/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/delete", httperror.LoggerHandler(h.deleteKubernetesJobs)).Methods(http.MethodPost)
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.
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
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("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
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
// from the Authorization token of the currently logged in user. The kubeclient that is not from the proxy is actually using
// admin permissions. If you're unsure which one to use, use this.
-func (h *Handler) getProxyKubeClient(r *http.Request) (*cli.KubeClient, *httperror.HandlerError) {
+func (h *Handler) getProxyKubeClient(r *http.Request) (portainer.KubeClient, *httperror.HandlerError) {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
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)
@@ -253,7 +255,7 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
return
}
serverURL.Scheme = "https"
- serverURL.Host = "localhost" + handler.KubernetesClientFactory.AddrHTTPS
+ serverURL.Host = "localhost" + handler.KubernetesClientFactory.GetAddrHTTPS()
config.Clusters[0].Cluster.Server = serverURL.String()
yaml, err := cli.GenerateYAML(config)
diff --git a/api/http/models/kubernetes/event.go b/api/http/models/kubernetes/event.go
new file mode 100644
index 000000000..be447b554
--- /dev/null
+++ b/api/http/models/kubernetes/event.go
@@ -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"`
+}
diff --git a/api/internal/testhelpers/kube_client.go b/api/internal/testhelpers/kube_client.go
new file mode 100644
index 000000000..550e7ce92
--- /dev/null
+++ b/api/internal/testhelpers/kube_client.go
@@ -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
+}
diff --git a/api/kubernetes/cli/access.go b/api/kubernetes/cli/access.go
index 73f8d50af..6f254c296 100644
--- a/api/kubernetes/cli/access.go
+++ b/api/kubernetes/cli/access.go
@@ -143,3 +143,23 @@ func (kcl *KubeClient) GetNonAdminNamespaces(userID int, teamIDs []int, isRestri
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
+}
diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go
index 6d2cc437c..a40a865f1 100644
--- a/api/kubernetes/cli/client.go
+++ b/api/kubernetes/cli/client.go
@@ -82,6 +82,10 @@ func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.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.
// 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) {
diff --git a/api/kubernetes/cli/event.go b/api/kubernetes/cli/event.go
new file mode 100644
index 000000000..03472fca6
--- /dev/null
+++ b/api/kubernetes/cli/event.go
@@ -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
+}
diff --git a/api/kubernetes/cli/event_test.go b/api/kubernetes/cli/event_test.go
new file mode 100644
index 000000000..926928317
--- /dev/null
+++ b/api/kubernetes/cli/event_test.go
@@ -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")
+ })
+}
diff --git a/api/portainer.go b/api/portainer.go
index f61bb1af2..f63d04ddb 100644
--- a/api/portainer.go
+++ b/api/portainer.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
+ "net/http"
"time"
"github.com/docker/docker/api/types"
@@ -13,6 +14,7 @@ import (
gittypes "github.com/portainer/portainer/api/git/types"
models "github.com/portainer/portainer/api/http/models/kubernetes"
"github.com/portainer/portainer/pkg/featureflags"
+ httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/segmentio/encoding/json"
"golang.org/x/oauth2"
@@ -1531,56 +1533,127 @@ type (
// KubeClient represents a service used to query a Kubernetes environment(endpoint)
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
- 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)
+ // Applications
+ GetApplications(namespace, nodeName string) ([]models.K8sApplication, error)
+ GetApplicationsResource(namespace, node string) (models.K8sApplicationResource, error)
+
+ // ClusterRole
+ GetClusterRoles() ([]models.K8sClusterRole, 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)
+ // ClusterRoleBinding
+ GetClusterRoleBindings() ([]models.K8sClusterRoleBinding, error)
+ DeleteClusterRoleBindings(reqs models.K8sClusterRoleBindingDeleteRequests) error
+
+ // Dashboard
+ GetDashboard() (models.K8sDashboard, error)
+
+ // Deployment
HasStackName(namespace string, stackName string) (bool, error)
- NamespaceAccessPoliciesDeleteNamespace(namespace string) error
- CreateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error)
- 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)
+
+ // Ingress
GetIngressControllers() (models.K8sIngressControllers, error)
- GetApplications(namespace, nodename string) ([]models.K8sApplication, error)
- GetMetrics() (models.K8sMetrics, error)
- GetStorage() ([]KubernetesStorageClassConfig, error)
- CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error
- UpdateIngress(namespace string, info models.K8sIngressInfo) error
+ GetIngress(namespace, ingressName string) (models.K8sIngressInfo, error)
GetIngresses(namespace string) ([]models.K8sIngressInfo, error)
+ CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error
DeleteIngresses(reqs models.K8sIngressDeleteRequests) error
- CreateService(namespace string, service models.K8sServiceInfo) error
- UpdateService(namespace string, service models.K8sServiceInfo) error
- GetServices(namespace string) ([]models.K8sServiceInfo, error)
- DeleteServices(reqs models.K8sServiceDeleteRequests) error
+ UpdateIngress(namespace string, info models.K8sIngressInfo) error
+ CombineIngressWithService(ingress models.K8sIngressInfo) (models.K8sIngressInfo, error)
+ CombineIngressesWithServices(ingresses []models.K8sIngressInfo) ([]models.K8sIngressInfo, 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)
- GetMaxResourceLimits(name string, overCommitEnabled bool, resourceOverCommitPercent int) (K8sNodeLimits, error)
- GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
- UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
+ GetMaxResourceLimits(skipNamespace string, overCommitEnabled bool, resourceOverCommitPercent int) (K8sNodeLimits, error)
+
+ // Pod
+ CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
+
+ // RBAC
+ IsRBACEnabled() (bool, error)
+
+ // Registries
DeleteRegistrySecret(registry RegistryID, namespace string) error
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
+ // RoleBinding
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)
diff --git a/app/kubernetes/views/configurations/configmap/edit/configMap.html b/app/kubernetes/views/configurations/configmap/edit/configMap.html
index 89e1c38c5..70a2d4f1a 100644
--- a/app/kubernetes/views/configurations/configmap/edit/configMap.html
+++ b/app/kubernetes/views/configurations/configmap/edit/configMap.html
@@ -58,7 +58,7 @@
diff --git a/app/kubernetes/views/configurations/secret/edit/secret.html b/app/kubernetes/views/configurations/secret/edit/secret.html
index 0309d356c..2e939c87e 100644
--- a/app/kubernetes/views/configurations/secret/edit/secret.html
+++ b/app/kubernetes/views/configurations/secret/edit/secret.html
@@ -65,7 +65,7 @@
diff --git a/app/react/kubernetes/components/EventsDatatable/EventsDatatable.test.tsx b/app/react/kubernetes/components/EventsDatatable/EventsDatatable.test.tsx
new file mode 100644
index 000000000..c5dc49c16
--- /dev/null
+++ b/app/react/kubernetes/components/EventsDatatable/EventsDatatable.test.tsx
@@ -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 = {
+ 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(() => (
+
+ )),
+ user
+ )
+ );
+ return { ...render(), 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
+ );
+ });
+});
diff --git a/app/react/kubernetes/components/EventsDatatable/EventsDatatable.tsx b/app/react/kubernetes/components/EventsDatatable/EventsDatatable.tsx
index ffb92e7bd..fd063535a 100644
--- a/app/react/kubernetes/components/EventsDatatable/EventsDatatable.tsx
+++ b/app/react/kubernetes/components/EventsDatatable/EventsDatatable.tsx
@@ -1,7 +1,7 @@
-import { Event } from 'kubernetes-types/core/v1';
import { History } from 'lucide-react';
import { ReactNode } from 'react';
+import { Event } from '@/react/kubernetes/queries/types';
import { IndexOptional } from '@/react/kubernetes/configs/types';
import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
@@ -38,7 +38,7 @@ export function EventsDatatable({
isLoading={isLoading}
title={title}
titleIcon={titleIcon}
- getRowId={(row) => row.metadata?.uid || ''}
+ getRowId={(row) => row.uid || ''}
disableSelect
renderTableSettings={() => (
diff --git a/app/react/kubernetes/components/EventsDatatable/ResourceEventsDatatable.tsx b/app/react/kubernetes/components/EventsDatatable/ResourceEventsDatatable.tsx
index f763043c9..bf04d00d9 100644
--- a/app/react/kubernetes/components/EventsDatatable/ResourceEventsDatatable.tsx
+++ b/app/react/kubernetes/components/EventsDatatable/ResourceEventsDatatable.tsx
@@ -29,9 +29,7 @@ export function ResourceEventsDatatable({
params: { endpointId },
} = useCurrentStateAndParams();
- const params = resourceId
- ? { fieldSelector: `involvedObject.uid=${resourceId}` }
- : {};
+ const params = resourceId ? { resourceId: `${resourceId}` } : {};
const resourceEventsQuery = useEvents(endpointId, {
namespace,
params,
diff --git a/app/react/kubernetes/components/EventsDatatable/columns/eventType.tsx b/app/react/kubernetes/components/EventsDatatable/columns/eventType.tsx
index 0d381d682..d2f24e3ab 100644
--- a/app/react/kubernetes/components/EventsDatatable/columns/eventType.tsx
+++ b/app/react/kubernetes/components/EventsDatatable/columns/eventType.tsx
@@ -1,5 +1,6 @@
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 { filterHOC } from '@@/datatables/Filter';
diff --git a/app/react/kubernetes/components/EventsDatatable/columns/helper.ts b/app/react/kubernetes/components/EventsDatatable/columns/helper.ts
index 23a453238..dbb2a57d5 100644
--- a/app/react/kubernetes/components/EventsDatatable/columns/helper.ts
+++ b/app/react/kubernetes/components/EventsDatatable/columns/helper.ts
@@ -1,4 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
-import { Event } from 'kubernetes-types/core/v1';
+
+import { Event } from '@/react/kubernetes/queries/types';
export const columnHelper = createColumnHelper();
diff --git a/app/react/kubernetes/components/EventsDatatable/columns/kind.tsx b/app/react/kubernetes/components/EventsDatatable/columns/kind.tsx
index 641662291..efc69fe4e 100644
--- a/app/react/kubernetes/components/EventsDatatable/columns/kind.tsx
+++ b/app/react/kubernetes/components/EventsDatatable/columns/kind.tsx
@@ -1,5 +1,6 @@
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';
diff --git a/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.test.tsx b/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.test.tsx
index 89f522d3a..9cad1d046 100644
--- a/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.test.tsx
+++ b/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.test.tsx
@@ -184,15 +184,8 @@ describe(
http.get('/api/endpoints/3/kubernetes/helm/test-release/history', () =>
HttpResponse.json(helmReleaseHistory)
),
- http.get(
- '/api/endpoints/3/kubernetes/api/v1/namespaces/default/events',
- () =>
- HttpResponse.json({
- kind: 'EventList',
- apiVersion: 'v1',
- metadata: { resourceVersion: '12345' },
- items: [],
- })
+ http.get('/api/kubernetes/3/namespaces/default/events', () =>
+ HttpResponse.json([])
)
);
@@ -236,15 +229,8 @@ describe(
HttpResponse.error()
),
// Add mock for events endpoint
- http.get(
- '/api/endpoints/3/kubernetes/api/v1/namespaces/default/events',
- () =>
- HttpResponse.json({
- kind: 'EventList',
- apiVersion: 'v1',
- metadata: { resourceVersion: '12345' },
- items: [],
- })
+ http.get('/api/kubernetes/3/namespaces/default/events', () =>
+ HttpResponse.json([])
)
);
@@ -274,15 +260,8 @@ describe(
http.get('/api/endpoints/3/kubernetes/helm/test-release/history', () =>
HttpResponse.json(helmReleaseHistory)
),
- http.get(
- '/api/endpoints/3/kubernetes/api/v1/namespaces/default/events',
- () =>
- HttpResponse.json({
- kind: 'EventList',
- apiVersion: 'v1',
- metadata: { resourceVersion: '12345' },
- items: [],
- })
+ http.get('/api/kubernetes/3/namespaces/default/events', () =>
+ HttpResponse.json([])
)
);
diff --git a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/HelmEventsDatatable.test.tsx b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/HelmEventsDatatable.test.tsx
index 08c48488a..67ff7d10b 100644
--- a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/HelmEventsDatatable.test.tsx
+++ b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/HelmEventsDatatable.test.tsx
@@ -1,7 +1,7 @@
import { render, screen, waitFor } from '@testing-library/react';
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 { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { withTestRouter } from '@/react/test-utils/withRouter';
@@ -56,136 +56,84 @@ const testResources: GenericResource[] = [
},
];
-const mockEventsResponse: EventList = {
- kind: 'EventList',
- apiVersion: 'v1',
- metadata: {
- resourceVersion: '12345',
+const mockEventsResponse: Event[] = [
+ {
+ name: 'test-deployment-123456',
+ namespace: 'default',
+ reason: 'CreatedLoadBalancer',
+ eventTime: new Date('2023-01-01T00:00:00Z'),
+ uid: 'event-uid-1',
+ involvedObject: {
+ kind: 'Deployment',
+ name: 'test-deployment',
+ uid: 'test-deployment-uid',
+ namespace: 'default',
+ },
+ message: 'Scaled up replica set test-deployment-abc123 to 1',
+ firstTimestamp: new Date('2023-01-01T00:00:00Z'),
+ lastTimestamp: new Date('2023-01-01T00:00:00Z'),
+ count: 1,
+ type: 'Normal',
},
- items: [
- {
- metadata: {
- name: 'test-deployment-123456',
- namespace: 'default',
- uid: 'event-uid-1',
- resourceVersion: '1000',
- creationTimestamp: '2023-01-01T00:00:00Z',
- },
- involvedObject: {
- kind: 'Deployment',
- namespace: 'default',
- name: 'test-deployment',
- uid: 'test-deployment-uid',
- apiVersion: 'apps/v1',
- resourceVersion: '2000',
- },
- reason: 'ScalingReplicaSet',
- message: 'Scaled up replica set test-deployment-abc123 to 1',
- source: {
- component: 'deployment-controller',
- },
- firstTimestamp: '2023-01-01T00:00:00Z',
- lastTimestamp: '2023-01-01T00:00:00Z',
- count: 1,
- type: 'Normal',
- reportingComponent: 'deployment-controller',
- reportingInstance: '',
+ {
+ name: 'test-service-123456',
+ namespace: 'default',
+ uid: 'event-uid-2',
+ eventTime: new Date('2023-01-01T00:00:00Z'),
+ involvedObject: {
+ kind: 'Service',
+ namespace: 'default',
+ name: 'test-service',
+ uid: 'test-service-uid',
},
- {
- metadata: {
- name: 'test-service-123456',
- namespace: 'default',
- uid: 'event-uid-2',
- resourceVersion: '1001',
- creationTimestamp: '2023-01-01T00:00:00Z',
- },
- involvedObject: {
- kind: 'Service',
- namespace: 'default',
- name: 'test-service',
- uid: 'test-service-uid',
- apiVersion: 'v1',
- resourceVersion: '2001',
- },
- reason: 'CreatedLoadBalancer',
- message: 'Created load balancer',
- source: {
- component: 'service-controller',
- },
- firstTimestamp: '2023-01-01T00:00:00Z',
- lastTimestamp: '2023-01-01T00:00:00Z',
- count: 1,
- type: 'Normal',
- reportingComponent: 'service-controller',
- reportingInstance: '',
- },
- ],
-};
+ reason: 'CreatedLoadBalancer',
+ message: 'Created load balancer',
+ firstTimestamp: new Date('2023-01-01T00:00:00Z'),
+ lastTimestamp: new Date('2023-01-01T00:00:00Z'),
+ count: 1,
+ type: 'Normal',
+ },
+];
-const mixedEventsResponse: EventList = {
- kind: 'EventList',
- apiVersion: 'v1',
- metadata: {
- resourceVersion: '12345',
+const mixedEventsResponse: Event[] = [
+ {
+ name: 'test-deployment-123456',
+ namespace: 'default',
+ uid: 'event-uid-1',
+ eventTime: new Date('2023-01-01T00:00:00Z'),
+ involvedObject: {
+ kind: 'Deployment',
+ namespace: 'default',
+ name: 'test-deployment',
+ uid: 'test-deployment-uid', // This matches a resource UID
+ },
+ reason: 'ScalingReplicaSet',
+ message: 'Scaled up replica set test-deployment-abc123 to 1',
+
+ firstTimestamp: new Date('2023-01-01T00:00:00Z'),
+ lastTimestamp: new Date('2023-01-01T00:00:00Z'),
+ count: 1,
+ type: 'Normal',
},
- items: [
- {
- metadata: {
- name: 'test-deployment-123456',
- namespace: 'default',
- uid: 'event-uid-1',
- resourceVersion: '1000',
- creationTimestamp: '2023-01-01T00:00:00Z',
- },
- involvedObject: {
- kind: 'Deployment',
- namespace: 'default',
- name: 'test-deployment',
- uid: 'test-deployment-uid', // This matches a resource UID
- apiVersion: 'apps/v1',
- resourceVersion: '2000',
- },
- reason: 'ScalingReplicaSet',
- message: 'Scaled up replica set test-deployment-abc123 to 1',
- source: {
- component: 'deployment-controller',
- },
- firstTimestamp: '2023-01-01T00:00:00Z',
- lastTimestamp: '2023-01-01T00:00:00Z',
- count: 1,
- type: 'Normal',
- reportingComponent: 'deployment-controller',
- reportingInstance: '',
+ {
+ name: 'unrelated-pod-123456',
+ namespace: 'default',
+ uid: 'event-uid-3',
+ eventTime: new Date('2023-01-01T00:00:00Z'),
+ involvedObject: {
+ kind: 'Pod',
+ namespace: 'default',
+ name: 'unrelated-pod',
+ uid: 'unrelated-pod-uid', // This does NOT match any resource UIDs
},
- {
- metadata: {
- name: 'unrelated-pod-123456',
- namespace: 'default',
- uid: 'event-uid-3',
- resourceVersion: '1002',
- creationTimestamp: '2023-01-01T00:00:00Z',
- },
- involvedObject: {
- kind: 'Pod',
- namespace: 'default',
- name: 'unrelated-pod',
- uid: 'unrelated-pod-uid', // This does NOT match any resource UIDs
- apiVersion: 'v1',
- resourceVersion: '2002',
- },
- reason: 'Scheduled',
- message: 'Successfully assigned unrelated-pod to node',
- source: {
- component: 'default-scheduler',
- },
- firstTimestamp: '2023-01-01T00:00:00Z',
- lastTimestamp: '2023-01-01T00:00:00Z',
- count: 1,
- reportingComponent: 'scheduler',
- reportingInstance: '',
- },
- ],
-};
+ reason: 'Scheduled',
+ message: 'Successfully assigned unrelated-pod to node',
+ type: 'Normal',
+ firstTimestamp: new Date('2023-01-01T00:00:00Z'),
+ lastTimestamp: new Date('2023-01-01T00:00:00Z'),
+ count: 1,
+ },
+];
function renderComponent() {
const user = new UserViewModel({ Username: 'user' });
@@ -229,7 +177,7 @@ describe('HelmEventsDatatable', () => {
it('should correctly filter related events using the filterRelatedEvents function', () => {
const filteredEvents = filterRelatedEvents(
- mixedEventsResponse.items as Event[],
+ mixedEventsResponse as Event[],
testResources
);
diff --git a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/HelmEventsDatatable.tsx b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/HelmEventsDatatable.tsx
index ced859f54..9dd161f5b 100644
--- a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/HelmEventsDatatable.tsx
+++ b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/HelmEventsDatatable.tsx
@@ -1,6 +1,6 @@
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 { EventsDatatable } from '@/react/kubernetes/components/EventsDatatable';
import { useEvents } from '@/react/kubernetes/queries/useEvents';
diff --git a/app/react/kubernetes/queries/types.ts b/app/react/kubernetes/queries/types.ts
new file mode 100644
index 000000000..4c8269c28
--- /dev/null
+++ b/app/react/kubernetes/queries/types.ts
@@ -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;
+ };
+};
diff --git a/app/react/kubernetes/queries/useEvents.ts b/app/react/kubernetes/queries/useEvents.ts
index f25c255db..47ea5c2c5 100644
--- a/app/react/kubernetes/queries/useEvents.ts
+++ b/app/react/kubernetes/queries/useEvents.ts
@@ -1,6 +1,6 @@
-import { EventList, Event } from 'kubernetes-types/core/v1';
import { useQuery } from '@tanstack/react-query';
+import { Event } from '@/react/kubernetes/queries/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios from '@/portainer/services/axios';
import { withGlobalError } from '@/react-tools/react-query';
@@ -13,10 +13,7 @@ type RequestOptions = {
/** if undefined, events are fetched at the cluster scope */
namespace?: string;
params?: {
- /** https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors */
- labelSelector?: string;
- /** https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors */
- fieldSelector?: string;
+ resourceId?: string;
};
};
@@ -44,13 +41,13 @@ async function getEvents(
): Promise {
const { namespace, params } = options ?? {};
try {
- const { data } = await axios.get(
+ const { data } = await axios.get(
buildUrl(environmentId, namespace),
{
params,
}
);
- return data.items;
+ return data;
} catch (e) {
throw parseKubernetesAxiosError(e, 'Unable to retrieve events');
}
@@ -96,6 +93,6 @@ export function useEventWarningsCount(
function buildUrl(environmentId: EnvironmentId, namespace?: string) {
return namespace
- ? `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/events`
- : `/endpoints/${environmentId}/kubernetes/api/v1/events`;
+ ? `/kubernetes/${environmentId}/namespaces/${namespace}/events`
+ : `/kubernetes/${environmentId}/events`;
}