Răsfoiți Sursa

separate query notifications into its own handler (#4070)

Feroze Mohideen 2 ani în urmă
părinte
comite
92279f84aa

+ 177 - 0
api/server/handlers/porter_app/app_notifications.go

@@ -0,0 +1,177 @@
+package porter_app
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+
+	"connectrpc.com/connect"
+
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+
+	"github.com/google/uuid"
+
+	"github.com/porter-dev/porter/internal/porter_app"
+	"github.com/porter-dev/porter/internal/porter_app/notifications"
+	"github.com/porter-dev/porter/internal/telemetry"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// AppNotificationsHandler handles requests to the /apps/{porter_app_name}/notifications endpoint
+type AppNotificationsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewAppNotificationsHandler returns a new AppNotificationsHandler
+func NewAppNotificationsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *AppNotificationsHandler {
+	return &AppNotificationsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// AppNotificationsRequest is the request object for the /apps/{porter_app_name}/notifications endpoint
+type AppNotificationsRequest struct {
+	DeploymentTargetID string `schema:"deployment_target_id"`
+}
+
+// AppNotificationsResponse is the response object for the /apps/{porter_app_name}/notifications endpoint
+type AppNotificationsResponse struct {
+	// Notifications are the notifications associated with the app revision
+	Notifications []notifications.Notification `json:"notifications"`
+}
+
+func (c *AppNotificationsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-app-notifications")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		e := telemetry.Error(ctx, span, reqErr, "error parsing stack name from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appName})
+
+	request := &AppNotificationsRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	_, err := uuid.Parse(request.DeploymentTargetID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error parsing deployment target id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID})
+
+	porterApps, err := c.Repo().PorterApp().ReadPorterAppsByProjectIDAndName(project.ID, appName)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting porter apps")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if len(porterApps) == 0 {
+		err := telemetry.Error(ctx, span, err, "no porter apps returned")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if len(porterApps) > 1 {
+		err := telemetry.Error(ctx, span, err, "multiple porter apps returned; unable to determine which one to use")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appId := porterApps[0].ID
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-id", Value: appId})
+
+	if appId == 0 {
+		err := telemetry.Error(ctx, span, err, "porter app id is missing")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	currentAppRevisionReq := connect.NewRequest(&porterv1.CurrentAppRevisionRequest{
+		ProjectId: int64(project.ID),
+		AppId:     int64(appId),
+		DeploymentTargetIdentifier: &porterv1.DeploymentTargetIdentifier{
+			Id: request.DeploymentTargetID,
+		},
+	})
+
+	currentAppRevisionResp, err := c.Config().ClusterControlPlaneClient.CurrentAppRevision(ctx, currentAppRevisionReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting current app revision from cluster control plane client")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if currentAppRevisionResp == nil || currentAppRevisionResp.Msg == nil {
+		err := telemetry.Error(ctx, span, err, "current app revision resp is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	appRevision := currentAppRevisionResp.Msg.AppRevision
+	encodedRevision, err := porter_app.EncodedRevisionFromProto(ctx, appRevision)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error encoding revision from proto")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	appRevisionId := encodedRevision.ID
+	appInstanceId := encodedRevision.AppInstanceID
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "app-revision-id", Value: appRevisionId},
+		telemetry.AttributeKV{Key: "app-instance-id", Value: appInstanceId},
+	)
+	notificationEvents, err := c.Repo().PorterAppEvent().ReadNotificationsByAppRevisionID(ctx, appInstanceId, appRevisionId)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting notifications from repo")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	latestNotifications := make([]notifications.Notification, 0)
+	for _, event := range notificationEvents {
+		notification, err := notifications.NotificationFromPorterAppEvent(event)
+		if err != nil {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "notification-conversion-error", Value: err.Error()})
+			continue
+		}
+		if notification == nil {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "notification-conversion-error", Value: "notification is nil"})
+			continue
+		}
+		// TODO: remove this check once this attribute is not found in the span for >30 days
+		if notification.Scope == "" {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "notification-conversion-error", Value: "old-notification-format"})
+			continue
+		}
+		latestNotifications = append(latestNotifications, *notification)
+	}
+
+	response := AppNotificationsResponse{
+		Notifications: latestNotifications,
+	}
+
+	c.WriteResult(w, r, response)
+}

+ 1 - 29
api/server/handlers/porter_app/current_app_revision.go

@@ -13,7 +13,6 @@ import (
 	"github.com/google/uuid"
 
 	"github.com/porter-dev/porter/internal/porter_app"
-	"github.com/porter-dev/porter/internal/porter_app/notifications"
 	"github.com/porter-dev/porter/internal/telemetry"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -51,8 +50,6 @@ type LatestAppRevisionRequest struct {
 type LatestAppRevisionResponse struct {
 	// AppRevision is the latest revision for the app
 	AppRevision porter_app.Revision `json:"app_revision"`
-	// Notifications are the notifications associated with the app revision
-	Notifications []notifications.Notification `json:"notifications"`
 }
 
 // ServeHTTP translates the request into a CurrentAppRevision grpc request, forwards to the cluster control plane, and returns the response.
@@ -152,34 +149,9 @@ func (c *LatestAppRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		telemetry.AttributeKV{Key: "app-revision-id", Value: appRevisionId},
 		telemetry.AttributeKV{Key: "app-instance-id", Value: appInstanceId},
 	)
-	notificationEvents, err := c.Repo().PorterAppEvent().ReadNotificationsByAppRevisionID(ctx, appInstanceId, appRevisionId)
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error getting notifications from repo")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
-	latestNotifications := make([]notifications.Notification, 0)
-	for _, event := range notificationEvents {
-		notification, err := notifications.NotificationFromPorterAppEvent(event)
-		if err != nil {
-			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "notification-conversion-error", Value: err.Error()})
-			continue
-		}
-		if notification == nil {
-			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "notification-conversion-error", Value: "notification is nil"})
-			continue
-		}
-		// TODO: remove this check once this attribute is not found in the span for >30 days
-		if notification.Scope == "" {
-			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "notification-conversion-error", Value: "old-notification-format"})
-			continue
-		}
-		latestNotifications = append(latestNotifications, *notification)
-	}
 
 	response := LatestAppRevisionResponse{
-		AppRevision:   encodedRevision,
-		Notifications: latestNotifications,
+		AppRevision: encodedRevision,
 	}
 
 	c.WriteResult(w, r, response)

+ 29 - 0
api/server/router/porter_app.go

@@ -833,6 +833,35 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/notifications -> porter_app.NewAppNotificationsHandler
+	appNotificationsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/notifications", relPathV2, types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	appNotificationsHandler := porter_app.NewAppNotificationsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: appNotificationsEndpoint,
+		Handler:  appNotificationsHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions -> porter_app.NewCurrentAppRevisionHandler
 	listAppRevisionsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 46 - 19
dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx

@@ -16,6 +16,7 @@ import Container from "components/porter/Container";
 import Link from "components/porter/Link";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
+import { type DeploymentTarget } from "lib/hooks/useDeploymentTarget";
 import { usePorterYaml } from "lib/hooks/usePorterYaml";
 import { clientAppFromProto, type SourceOptions } from "lib/porter-apps";
 import {
@@ -30,9 +31,7 @@ import { appRevisionValidator, type AppRevision } from "lib/revisions/types";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
-import {
-  useDeploymentTarget,
-} from "shared/DeploymentTargetContext";
+import { useDeploymentTarget } from "shared/DeploymentTargetContext";
 import { valueExists } from "shared/util";
 import notFound from "assets/not-found.png";
 
@@ -42,7 +41,6 @@ import {
 } from "../validate-apply/app-settings/types";
 import { porterAppValidator, type PorterAppRecord } from "./AppView";
 import { porterAppNotificationEventMetadataValidator } from "./tabs/activity-feed/events/types";
-import {type DeploymentTarget} from "lib/hooks/useDeploymentTarget";
 
 type LatestRevisionContextType = {
   porterApp: PorterAppRecord;
@@ -121,13 +119,7 @@ export const LatestRevisionProvider: React.FC<LatestRevisionProviderProps> = ({
     }
   );
 
-  const {
-    data: {
-      app_revision: latestRevision,
-      notifications: latestPorterAppNotifications = [],
-    } = {},
-    status,
-  } = useQuery(
+  const { data: { app_revision: latestRevision } = {}, status } = useQuery(
     [
       "getLatestRevision",
       currentProject?.id,
@@ -136,9 +128,8 @@ export const LatestRevisionProvider: React.FC<LatestRevisionProviderProps> = ({
       appName,
     ],
     async () => {
-
       if (!appParamsExist) {
-        return { app_revision: undefined, notifications: [] };
+        return { app_revision: undefined };
       }
       const res = await api.getLatestRevision(
         "<token>",
@@ -152,18 +143,13 @@ export const LatestRevisionProvider: React.FC<LatestRevisionProviderProps> = ({
         }
       );
 
-      const {
-        app_revision: appRevision,
-        notifications: porterAppNotifications,
-      } = await z
+      const { app_revision: appRevision } = await z
         .object({
           app_revision: appRevisionValidator,
-          notifications: z.array(porterAppNotificationEventMetadataValidator),
         })
         .parseAsync(res.data);
       return {
         app_revision: appRevision,
-        notifications: porterAppNotifications,
       };
     },
     {
@@ -173,6 +159,47 @@ export const LatestRevisionProvider: React.FC<LatestRevisionProviderProps> = ({
     }
   );
 
+  const { data: { notifications: latestPorterAppNotifications = [] } = {} } =
+    useQuery(
+      [
+        "appNotifications",
+        currentProject?.id,
+        currentCluster?.id,
+        currentDeploymentTarget,
+        appName,
+      ],
+      async () => {
+        if (!appParamsExist) {
+          return { notifications: [] };
+        }
+        const res = await api.appNotifications(
+          "<token>",
+          {
+            deployment_target_id: currentDeploymentTarget.id,
+          },
+          {
+            project_id: currentProject.id,
+            cluster_id: currentCluster.id,
+            porter_app_name: appName,
+          }
+        );
+
+        const { notifications: porterAppNotifications } = await z
+          .object({
+            notifications: z.array(porterAppNotificationEventMetadataValidator),
+          })
+          .parseAsync(res.data);
+        return {
+          notifications: porterAppNotifications,
+        };
+      },
+      {
+        enabled: appParamsExist,
+        refetchInterval: 5000,
+        refetchOnWindowFocus: false,
+      }
+    );
+
   const revisionId = previewRevision?.id ?? latestRevision?.id;
   const { data: { attachedEnvGroups = [], appEnv } = {} } = useQuery(
     ["getAttachedEnvGroups", appName, revisionId],

+ 0 - 6
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts

@@ -47,8 +47,6 @@ const porterAppPreDeployEventMetadataValidator = z.object({
 
 const serviceNoticationValidator = z.object({
   id: z.string(),
-  app_id: z.string(),
-  app_name: z.string(),
   app_revision_id: z.string(),
   error: z.object({
     code: z.number(),
@@ -85,8 +83,6 @@ const serviceNoticationValidator = z.object({
 });
 const revisionNotificationValidator = z.object({
   id: z.string(),
-  app_id: z.string(),
-  app_name: z.string(),
   app_revision_id: z.string(),
   error: z.object({
     code: z.number(),
@@ -100,8 +96,6 @@ const revisionNotificationValidator = z.object({
 });
 const applicationNotificationValidator = z.object({
   id: z.string(),
-  app_id: z.string(),
-  app_name: z.string(),
   app_revision_id: z.string(),
   error: z.object({
     code: z.number(),

+ 14 - 0
dashboard/src/shared/api.tsx

@@ -1117,6 +1117,19 @@ const getLatestRevision = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/apps/${porter_app_name}/latest`;
 });
 
+const appNotifications = baseApi<
+  {
+    deployment_target_id: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    porter_app_name: string;
+  }
+>("GET", ({ project_id, cluster_id, porter_app_name }) => {
+  return `/api/projects/${project_id}/clusters/${cluster_id}/apps/${porter_app_name}/notifications`;
+});
+
 const getRevision = baseApi<
   {},
   {
@@ -3396,6 +3409,7 @@ export default {
   revertApp,
   getAttachedEnvGroups,
   getLatestRevision,
+  appNotifications,
   getRevision,
   listAppRevisions,
   getLatestAppRevisions,

+ 0 - 4
internal/porter_app/notifications/notification.go

@@ -8,10 +8,6 @@ import (
 
 // Notification is a struct that contains all actionable information from an app event
 type Notification struct {
-	// AppID is the ID of the app
-	AppID string `json:"app_id"`
-	// AppName is the name of the app
-	AppName string `json:"app_name"`
 	// AppRevisionID is the ID of the app revision that the notification belongs to
 	AppRevisionID string `json:"app_revision_id"`
 	// Error is the Porter error parsed from the agent event