Feroze Mohideen před 2 roky
rodič
revize
bd98c80d66

+ 171 - 19
api/server/handlers/porter_app/create_and_update_events.go

@@ -2,7 +2,9 @@ package porter_app
 
 
 import (
 import (
 	"context"
 	"context"
+	"fmt"
 	"net/http"
 	"net/http"
+	"strings"
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz"
@@ -75,7 +77,7 @@ func (p *CreateUpdatePorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *
 
 
 	event, err := p.updateExistingAppEvent(ctx, *cluster, stackName, *request)
 	event, err := p.updateExistingAppEvent(ctx, *cluster, stackName, *request)
 	if err != nil {
 	if err != nil {
-		e := telemetry.Error(ctx, span, err, "error creating new app event")
+		e := telemetry.Error(ctx, span, err, "error updating existing app event")
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 		return
 		return
 	}
 	}
@@ -99,25 +101,30 @@ func (p *CreateUpdatePorterAppEventHandler) createNewAppEvent(ctx context.Contex
 	)
 	)
 
 
 	if eventType == string(types.PorterAppEventType_AppEvent) {
 	if eventType == string(types.PorterAppEventType_AppEvent) {
-		// Agent has no way to know what the porter app event id is, so if we must dedup here
-		// TODO: create a filter to filter by only agent events. Not an issue now as app events are deduped per hour on the agent side
-		if agentEventID, ok := requestMetadata["agent_event_id"]; ok {
-			existingEvents, _, err := p.Repo().PorterAppEvent().ListEventsByPorterAppID(ctx, app.ID)
-			if err != nil {
-				return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error listing porter app events for event type")
-			}
+		if _, ok := requestMetadata["deploy_status"]; ok {
+			// update the deploy event if it exists
+			return p.maybeUpdateDeployEvent(ctx, porterAppName, app.ID, requestMetadata), nil
+		} else {
+			// Agent has no way to know what the porter app event id is, so if we must dedup here
+			// TODO: create a filter to filter by only agent events. Not an issue now as app events are deduped per hour on the agent side
+			if agentEventID, ok := requestMetadata["agent_event_id"]; ok {
+				existingEvents, _, err := p.Repo().PorterAppEvent().ListEventsByPorterAppID(ctx, app.ID)
+				if err != nil {
+					return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error listing porter app events for event type")
+				}
 
 
-			for _, existingEvent := range existingEvents {
-				if existingEvent.Type == eventType {
-					existingAgentEventID, ok := existingEvent.Metadata["agent_event_id"]
-					if !ok {
-						continue
-					}
-					if existingAgentEventID == 0 {
-						continue
-					}
-					if existingAgentEventID == agentEventID {
-						return existingEvent.ToPorterAppEvent(), nil
+				for _, existingEvent := range existingEvents {
+					if existingEvent.Type == eventType {
+						existingAgentEventID, ok := existingEvent.Metadata["agent_event_id"]
+						if !ok {
+							continue
+						}
+						if existingAgentEventID == 0 {
+							continue
+						}
+						if existingAgentEventID == agentEventID {
+							return existingEvent.ToPorterAppEvent(), nil
+						}
 					}
 					}
 				}
 				}
 			}
 			}
@@ -194,3 +201,148 @@ func (p *CreateUpdatePorterAppEventHandler) updateExistingAppEvent(ctx context.C
 
 
 	return existingAppEvent.ToPorterAppEvent(), nil
 	return existingAppEvent.ToPorterAppEvent(), nil
 }
 }
+
+func (p *CreateUpdatePorterAppEventHandler) maybeUpdateDeployEvent(ctx context.Context, appName string, appID uint, requestMetadata map[string]any) types.PorterAppEvent {
+	ctx, span := telemetry.NewSpan(ctx, "update-deploy-event")
+	defer span.End()
+
+	revision, ok := requestMetadata["revision"]
+	if !ok {
+		_ = telemetry.Error(ctx, span, nil, "revision not found in request metadata")
+		return types.PorterAppEvent{}
+	}
+	revisionFloat64, ok := revision.(float64)
+	if !ok {
+		_ = telemetry.Error(ctx, span, nil, "revision not a float64")
+		return types.PorterAppEvent{}
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "revision", Value: revisionFloat64})
+
+	podName, ok := requestMetadata["pod_name"]
+	if !ok {
+		_ = telemetry.Error(ctx, span, nil, "pod name not found in request metadata")
+		return types.PorterAppEvent{}
+	}
+	podNameStr, ok := podName.(string)
+	if !ok {
+		_ = telemetry.Error(ctx, span, nil, "pod name not a string")
+		return types.PorterAppEvent{}
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "pod-name", Value: podNameStr})
+
+	serviceName := getServiceName(podNameStr, appName)
+	if serviceName == "" {
+		_ = telemetry.Error(ctx, span, nil, "service name not found in pod name")
+		return types.PorterAppEvent{}
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "service-name", Value: serviceName})
+
+	newStatus, ok := requestMetadata["deploy_status"]
+	if !ok {
+		_ = telemetry.Error(ctx, span, nil, "deploy status not found in request metadata")
+		return types.PorterAppEvent{}
+	}
+	newStatusStr, ok := newStatus.(string)
+	if !ok {
+		_ = telemetry.Error(ctx, span, nil, "deploy status not a string")
+		return types.PorterAppEvent{}
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "new-status", Value: newStatusStr})
+
+	existingEvents, _, err := p.Repo().PorterAppEvent().ListEventsByPorterAppIDAndType(ctx, appID, string(types.PorterAppEventType_Deploy))
+	if err != nil {
+		_ = telemetry.Error(ctx, span, err, "error listing porter app events for deploy event type")
+		return types.PorterAppEvent{}
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-deployment-event", Value: false})
+
+	var matchEvent *models.PorterAppEvent
+	for _, existingEvent := range existingEvents {
+		if existingEvent.Status != "PROGRESSING" {
+			continue
+		}
+		if _, ok := existingEvent.Metadata["revision"]; !ok {
+			continue
+		}
+		if existingEvent.Metadata["revision"] == revisionFloat64 {
+			matchEvent = existingEvent
+			break
+		}
+	}
+
+	if matchEvent != nil {
+		serviceStatus, ok := matchEvent.Metadata["service_status"]
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "service status not found in deploy event metadata")
+			return types.PorterAppEvent{}
+		}
+		serviceStatusMap, ok := serviceStatus.(map[string]interface{})
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "service status not a map[string]interface")
+			return types.PorterAppEvent{}
+		}
+		if _, ok := serviceStatusMap[serviceName]; !ok {
+			_ = telemetry.Error(ctx, span, nil, fmt.Sprintf("service status not found for service %s", serviceName))
+			return types.PorterAppEvent{}
+		}
+
+		// only update service status if it has not been updated yet
+		if serviceStatusMap[serviceName] == "PROGRESSING" {
+			serviceStatusMap[serviceName] = newStatusStr
+
+			allServicesDone := true
+			anyServicesFailed := false
+			for _, status := range serviceStatusMap {
+				if status == "PROGRESSING" {
+					allServicesDone = false
+					break
+				}
+				if status == "FAILED" {
+					anyServicesFailed = true
+				}
+			}
+			if allServicesDone {
+				if anyServicesFailed {
+					matchEvent.Status = "FAILED"
+				} else {
+					matchEvent.Status = "SUCCESS"
+				}
+			}
+
+			matchEvent.Metadata["service_status"] = serviceStatusMap
+			err = p.Repo().PorterAppEvent().UpdateEvent(ctx, matchEvent)
+			if err != nil {
+				_ = telemetry.Error(ctx, span, err, "error updating deploy event")
+				return matchEvent.ToPorterAppEvent()
+			}
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-deployment-event", Value: true})
+		}
+	}
+
+	return types.PorterAppEvent{}
+}
+
+func getServiceName(podName, porterAppName string) string {
+	prefix := porterAppName + "-"
+	if !strings.HasPrefix(podName, prefix) {
+		return ""
+	}
+
+	podName = strings.TrimPrefix(podName, prefix)
+	suffixes := []string{"-web", "-wkr", "-job"}
+	index := -1
+
+	for _, suffix := range suffixes {
+		newIndex := strings.LastIndex(podName, suffix)
+		if newIndex > index {
+			index = newIndex
+		}
+	}
+
+	if index != -1 {
+		return podName[:index]
+	}
+
+	return ""
+}

+ 4 - 0
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx

@@ -28,6 +28,8 @@ type Props = {
   eventId?: string;
   eventId?: string;
 };
 };
 
 
+const EVENTS_POLL_INTERVAL = 10000;
+
 const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData, eventId }) => {
 const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData, eventId }) => {
   const { currentProject, currentCluster } = useContext(Context);
   const { currentProject, currentCluster } = useContext(Context);
 
 
@@ -91,7 +93,9 @@ const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData, eventId }) =
     if (!hasPorterAgent) {
     if (!hasPorterAgent) {
       checkForAgent();
       checkForAgent();
     } else {
     } else {
+      const intervalId = setInterval(getEvents, EVENTS_POLL_INTERVAL);
       getEvents();
       getEvents();
+      return () => clearInterval(intervalId);
     }
     }
 
 
   }, [currentProject, currentCluster, hasPorterAgent, page, eventId]);
   }, [currentProject, currentCluster, hasPorterAgent, page, eventId]);

+ 65 - 16
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/DeployEventCard.tsx

@@ -25,18 +25,55 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
   const [revertModalVisible, setRevertModalVisible] = useState(false);
   const [revertModalVisible, setRevertModalVisible] = useState(false);
   const [serviceStatusVisible, setServiceStatusVisible] = useState(false);
   const [serviceStatusVisible, setServiceStatusVisible] = useState(false);
 
 
-  const renderStatusText = (event: PorterAppDeployEvent) => {
+  const renderStatusText = () => {
     switch (event.status) {
     switch (event.status) {
       case "SUCCESS":
       case "SUCCESS":
-        return event.metadata.image_tag != null ? <Text color="#68BF8B">Deployed <Code>{event.metadata.image_tag}</Code></Text> : <Text color="#68BF8B">Deployment successful</Text>;
+        return event.metadata.image_tag != null ?
+          event.metadata.service_status != null ?
+            <Text color="#68BF8B">
+              Deployed <Code>{event.metadata.image_tag}</Code> to {Object.keys(event.metadata.service_status).length} service{Object.keys(event.metadata.service_status).length === 1 ? "" : "s"}
+            </Text> :
+            <Text color="#68BF8B">
+              Deployed <Code>{event.metadata.image_tag}</Code>
+            </Text>
+          :
+          <Text color="#68BF8B">
+            Deployment successful
+          </Text>;
       case "FAILED":
       case "FAILED":
-        return <Text color="#FF6060">Deployment failed</Text>;
+        if (event.metadata.service_status != null) {
+          let failedServices = 0;
+          for (const key in event.metadata.service_status) {
+            if (event.metadata.service_status[key] === "FAILED") {
+              failedServices++;
+            }
+          }
+          return (
+            <Text color="#FF6060">
+              Failed to deploy <Code>{event.metadata.image_tag}</Code> to {failedServices} service{failedServices === 1 ? "" : "s"}
+            </Text>
+          );
+        } else {
+          return (
+            <Text color="#FF6060">
+              Deployment failed
+            </Text>
+          );
+        }
       default:
       default:
-        return (
-          <Text color="helper">
-            Deploying <Code>{event.metadata.image_tag}</Code> to {Object.keys(event.metadata.service_status).length} service{Object.keys(event.metadata.service_status).length === 1 ? "" : "s"}...
-          </Text>
-        );
+        if (event.metadata.service_status != null) {
+          return (
+            <Text color="helper">
+              Deploying <Code>{event.metadata.image_tag}</Code> to {Object.keys(event.metadata.service_status).length} service{Object.keys(event.metadata.service_status).length === 1 ? "" : "s"}...
+            </Text>
+          );
+        } else {
+          return (
+            <Text color="helper">
+              Deploying <Code>{event.metadata.image_tag}</Code> to {Object.keys(event.metadata.service_status).length} service{Object.keys(event.metadata.service_status).length === 1 ? "" : "s"}...
+            </Text>
+          );
+        }
     }
     }
   };
   };
 
 
@@ -53,13 +90,17 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
     return Object.keys(serviceStatus).map((key) => {
     return Object.keys(serviceStatus).map((key) => {
       return (
       return (
         <Container key={key} row>
         <Container key={key} row>
-          <Spacer inline x={1} />
-          <Icon height="16px" src={getStatusIcon(serviceStatus[key])} />
           <Spacer inline x={1} />
           <Spacer inline x={1} />
           <Container row>
           <Container row>
-            <Text>{key}</Text>
+            <ServiceStatusContainer>
+              <Text>{key}</Text>
+            </ServiceStatusContainer>
             <Spacer inline x={1} />
             <Spacer inline x={1} />
-            <Text color="helper">{serviceStatus[key]}</Text>
+            <ServiceStatusContainer>
+              <Icon height="16px" src={getStatusIcon(serviceStatus[key])} />
+              <Spacer inline x={0.5} />
+              <Text color="helper">{serviceStatus[key]}</Text>
+            </ServiceStatusContainer>
           </Container>
           </Container>
         </Container>
         </Container>
       );
       );
@@ -79,7 +120,7 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
         <Container row>
         <Container row>
           <Icon height="16px" src={getStatusIcon(event.status)} />
           <Icon height="16px" src={getStatusIcon(event.status)} />
           <Spacer inline width="10px" />
           <Spacer inline width="10px" />
-          {renderStatusText(event)}
+          {renderStatusText()}
           {event.metadata.service_status != null &&
           {event.metadata.service_status != null &&
             <>
             <>
               <Spacer inline x={1} />
               <Spacer inline x={1} />
@@ -90,12 +131,12 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
               </TempWrapper>
               </TempWrapper>
             </>
             </>
           }
           }
-          {appData?.chart?.version !== event.metadata?.revision && (
+          {appData?.chart?.version !== event.metadata.revision && (
             <>
             <>
               <Spacer inline x={1} />
               <Spacer inline x={1} />
               <TempWrapper>
               <TempWrapper>
                 <Link hasunderline onClick={() => setRevertModalVisible(true)}>
                 <Link hasunderline onClick={() => setRevertModalVisible(true)}>
-                  Revert to version {event?.metadata?.revision}
+                  Revert to version {event.metadata.revision}
                 </Link>
                 </Link>
 
 
               </TempWrapper>
               </TempWrapper>
@@ -103,7 +144,7 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
           )}
           )}
           <Spacer inline x={1} />
           <Spacer inline x={1} />
           <TempWrapper>
           <TempWrapper>
-            {event?.metadata?.revision != 1 && (<Link hasunderline onClick={() => setDiffModalVisible(true)}>
+            {event.metadata.revision != 1 && (<Link hasunderline onClick={() => setDiffModalVisible(true)}>
               View changes
               View changes
             </Link>)}
             </Link>)}
             {diffModalVisible && (
             {diffModalVisible && (
@@ -147,3 +188,11 @@ const Code = styled.span`
   font-family: monospace;
   font-family: monospace;
 `;
 `;
 
 
+const ServiceStatusContainer = styled.div`
+  display: flex;
+  align-items: center;  
+  width: 150px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+`;

+ 14 - 13
dashboard/src/main/home/app-dashboard/expanded-app/logs/utils.ts

@@ -60,6 +60,7 @@ export const useLogs = (
     nextCursor: null,
     nextCursor: null,
   });
   });
 
 
+  // if currentPodName is empty assume we are looking at all chart pod logs
   const currentPod =
   const currentPod =
     currentPodName == ""
     currentPodName == ""
       ? currentChart?.name
       ? currentChart?.name
@@ -230,22 +231,22 @@ export const useLogs = (
       end_range: endDate,
       end_range: endDate,
       limit,
       limit,
       chart_name: "",
       chart_name: "",
-      pod_selector: currentPodName,
+      pod_selector: currentPod + "-.*",
       direction,
       direction,
     };
     };
 
 
-    if (currentPodName === "") {
-      if (currentChart == null) {
-        return {
-          logs: [],
-          previousCursor: null,
-          nextCursor: null,
-        };
-      } else if (currentChart.name.endsWith("-r")) {
-        getLogsReq.chart_name = currentChart.name;
-      } else {
-        getLogsReq.pod_selector = currentPod + "-.*";
-      }
+    if (currentChart == null) {
+      return {
+        logs: [],
+        previousCursor: null,
+        nextCursor: null,
+      };
+    }
+
+    // special casing for pre-deploy logs - see get_logs_within_time_range.go
+    if (currentChart.name.endsWith("-r")) {
+      getLogsReq.chart_name = currentChart.name;
+      getLogsReq.pod_selector = "";
     }
     }
 
 
     try {
     try {

+ 22 - 0
internal/repository/gorm/porter_app_event.go

@@ -46,6 +46,28 @@ func (repo *PorterAppEventRepository) ListEventsByPorterAppID(ctx context.Contex
 	return apps, paginatedResult, nil
 	return apps, paginatedResult, nil
 }
 }
 
 
+func (repo *PorterAppEventRepository) ListEventsByPorterAppIDAndType(ctx context.Context, porterAppID uint, eventType string, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error) {
+	apps := []*models.PorterAppEvent{}
+	paginatedResult := helpers.PaginatedResult{}
+
+	id := strconv.Itoa(int(porterAppID))
+	if id == "" {
+		return nil, paginatedResult, errors.New("invalid porter app id supplied")
+	}
+
+	db := repo.db.Model(&models.PorterAppEvent{})
+	resultDB := db.Where("porter_app_id = ? AND type = ?", id, eventType).Order("created_at DESC")
+	resultDB = resultDB.Scopes(helpers.Paginate(db, &paginatedResult, opts...))
+
+	if err := resultDB.Find(&apps).Error; err != nil {
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, paginatedResult, err
+		}
+	}
+
+	return apps, paginatedResult, nil
+}
+
 func (repo *PorterAppEventRepository) CreateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error {
 func (repo *PorterAppEventRepository) CreateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error {
 	if appEvent.ID == uuid.Nil {
 	if appEvent.ID == uuid.Nil {
 		appEvent.ID = uuid.New()
 		appEvent.ID = uuid.New()

+ 1 - 0
internal/repository/porter_app_event.go

@@ -11,6 +11,7 @@ import (
 // PorterAppEventRepository represents the set of queries on the PorterAppEvent model
 // PorterAppEventRepository represents the set of queries on the PorterAppEvent model
 type PorterAppEventRepository interface {
 type PorterAppEventRepository interface {
 	ListEventsByPorterAppID(ctx context.Context, porterAppID uint, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error)
 	ListEventsByPorterAppID(ctx context.Context, porterAppID uint, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error)
+	ListEventsByPorterAppIDAndType(ctx context.Context, porterAppID uint, eventType string, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error)
 	CreateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error
 	CreateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error
 	UpdateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error
 	UpdateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error
 	ReadEvent(ctx context.Context, id uuid.UUID) (models.PorterAppEvent, error)
 	ReadEvent(ctx context.Context, id uuid.UUID) (models.PorterAppEvent, error)