Преглед изворни кода

POR-2041: add defer func to update that cancels builds if workflow is canceled; handle cancel on front end (#3940)

Co-authored-by: David Townley <davidtownley@Davids-MacBook-Air.local>
d-g-town пре 2 година
родитељ
комит
bc6c6c6fb0

+ 5 - 0
cli/cmd/commands/errors.go

@@ -79,6 +79,11 @@ func checkLoginAndRunWithConfig(cmd *cobra.Command, cliConf config.CLIConfig, ar
 			return nil
 		}
 
+		if errors.Is(err, context.Canceled) {
+			color.New(color.FgYellow).Println("Command was canceled") // nolint:errcheck,gosec
+			return nil
+		}
+
 		cliErrors.GetErrorHandler(cliConf).HandleError(err)
 
 		return err

+ 52 - 23
cli/cmd/v2/update.go

@@ -6,8 +6,10 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"os/signal"
 	"path/filepath"
 	"strconv"
+	"syscall"
 	"time"
 
 	"github.com/porter-dev/porter/api/server/handlers/porter_app"
@@ -35,6 +37,20 @@ type UpdateInput struct {
 
 // Update implements the functionality of the `porter apply` command for validate apply v2 projects
 func Update(ctx context.Context, inp UpdateInput) error {
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	go func() {
+		termChan := make(chan os.Signal, 1)
+		signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
+		select {
+		case <-termChan:
+			color.New(color.FgYellow).Printf("Shutdown signal received, cancelling processes\n") // nolint:errcheck,gosec
+			cancel()
+		case <-ctx.Done():
+		}
+	}()
+
 	cliConf := inp.CLIConfig
 	client := inp.Client
 
@@ -113,16 +129,34 @@ func Update(ctx context.Context, inp UpdateInput) error {
 	if buildSettings != nil && buildSettings.Build.Method != "" {
 		eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, commitSHA)
 
-		reportBuildFailureInput := reportBuildFailureInput{
-			client:             client,
-			appName:            appName,
-			cliConf:            cliConf,
-			deploymentTargetID: deploymentTargetID,
-			appRevisionID:      updateResp.AppRevisionId,
-			eventID:            eventID,
-			commitSHA:          commitSHA,
-			prNumber:           prNumber,
-		}
+		var buildFinished bool
+		var buildError error
+		var buildLogs string
+
+		defer func() {
+			if buildError != nil && !errors.Is(buildError, context.Canceled) {
+				reportBuildFailureInput := reportBuildFailureInput{
+					client:             client,
+					appName:            appName,
+					cliConf:            cliConf,
+					deploymentTargetID: deploymentTargetID,
+					appRevisionID:      updateResp.AppRevisionId,
+					eventID:            eventID,
+					commitSHA:          commitSHA,
+					prNumber:           prNumber,
+					buildError:         buildError,
+					buildLogs:          buildLogs,
+				}
+				_ = reportBuildFailure(ctx, reportBuildFailureInput)
+				return
+			}
+			if !buildFinished {
+				buildMetadata := make(map[string]interface{})
+				buildMetadata["end_time"] = time.Now().UTC()
+				_ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, types.PorterAppEventType_Build, eventID, types.PorterAppEventStatus_Canceled, buildMetadata)
+				return
+			}
+		}()
 
 		if commitSHA == "" {
 			return errors.New("build is required but commit SHA cannot be identified. Please set the PORTER_COMMIT_SHA environment variable or run apply in git repository with access to the git CLI")
@@ -132,27 +166,21 @@ func Update(ctx context.Context, inp UpdateInput) error {
 
 		buildInput, err := buildInputFromBuildSettings(cliConf.Project, appName, commitSHA, buildSettings.Image, buildSettings.Build)
 		if err != nil {
-			err := fmt.Errorf("error creating build input from build settings: %w", err)
-			reportBuildFailureInput.buildError = err
-			_ = reportBuildFailure(ctx, reportBuildFailureInput)
-			return err
+			buildError = fmt.Errorf("error creating build input from build settings: %w", err)
+			return buildError
 		}
 
 		buildOutput := build(ctx, client, buildInput)
 		if buildOutput.Error != nil {
-			err := fmt.Errorf("error building app: %w", buildOutput.Error)
-			reportBuildFailureInput.buildLogs = buildOutput.Logs
-			reportBuildFailureInput.buildError = buildOutput.Error
-			_ = reportBuildFailure(ctx, reportBuildFailureInput)
-			return err
+			buildError = fmt.Errorf("error building app: %w", buildOutput.Error)
+			buildLogs = buildOutput.Logs
+			return buildError
 		}
 
 		_, err = client.UpdateRevisionStatus(ctx, cliConf.Project, cliConf.Cluster, appName, updateResp.AppRevisionId, models.AppRevisionStatus_BuildSuccessful)
 		if err != nil {
-			err := fmt.Errorf("error updating revision status post build: %w", err)
-			reportBuildFailureInput.buildError = err
-			_ = reportBuildFailure(ctx, reportBuildFailureInput)
-			return err
+			buildError = fmt.Errorf("error updating revision status post build: %w", err)
+			return buildError
 		}
 
 		color.New(color.FgGreen).Printf("Successfully built image (tag: %s)\n", buildSettings.Image.Tag) // nolint:errcheck,gosec
@@ -160,6 +188,7 @@ func Update(ctx context.Context, inp UpdateInput) error {
 		buildMetadata := make(map[string]interface{})
 		buildMetadata["end_time"] = time.Now().UTC()
 		_ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, types.PorterAppEventType_Build, eventID, types.PorterAppEventStatus_Success, buildMetadata)
+		buildFinished = true
 	}
 
 	color.New(color.FgGreen).Printf("Deploying new revision %s for app %s...\n", updateResp.AppRevisionId, appName) // nolint:errcheck,gosec

+ 32 - 25
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/BuildEventCard.tsx

@@ -14,10 +14,10 @@ import Icon from "components/porter/Icon";
 import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils';
 import { Code, ImageTagContainer, CommitIcon, StyledEventCard } from "./EventCard";
 import document from "assets/document.svg";
-import { PorterAppBuildEvent } from "../types";
+import { type PorterAppBuildEvent } from "../types";
 import { match } from "ts-pattern";
 import pull_request_icon from "assets/pull_request_icon.svg";
-import { PorterAppRecord } from "main/home/app-dashboard/app-view/AppView";
+import { type PorterAppRecord } from "main/home/app-dashboard/app-view/AppView";
 
 type Props = {
   event: PorterAppBuildEvent;
@@ -45,40 +45,47 @@ const BuildEventCard: React.FC<Props> = ({
         {match(event.status)
           .with("SUCCESS", () => "Build successful")
           .with("FAILED", () => "Build failed")
+          .with("CANCELED", () => "Build canceled")
           .otherwise(() => "Build in progress...")
         }
       </StatusContainer>
     );
   };
 
+  const renderLogsAndRetry = (event: PorterAppBuildEvent) => {
+    return (
+        <Wrapper>
+          <Link to={`/apps/${appName}/events?event_id=${event.id}`} hasunderline>
+            <Container row>
+              <Icon src={document} height="10px" />
+              <Spacer inline width="5px" />
+              View logs
+            </Container>
+          </Link>
+          <Spacer inline x={1} />
+          <Link hasunderline onClick={async () => { await triggerWorkflow({
+            projectId,
+            clusterId,
+            porterApp,
+          }); }}>
+            <Container row>
+              <Icon height="10px" src={refresh} />
+              <Spacer inline width="5px" />
+              Retry
+            </Container>
+          </Link>
+        </Wrapper>
+    );
+  }
+
   const renderInfoCta = (event: PorterAppBuildEvent) => {
     switch (event.status) {
       case "SUCCESS":
         return null;
+      case "CANCELED":
+        return renderLogsAndRetry(event);
       case "FAILED":
-        return (
-          <Wrapper>
-            <Link to={`/apps/${appName}/events?event_id=${event.id}`} hasunderline>
-              <Container row>
-                <Icon src={document} height="10px" />
-                <Spacer inline width="5px" />
-                View logs
-              </Container>
-            </Link>
-            <Spacer inline x={1} />
-            <Link hasunderline onClick={() => triggerWorkflow({
-              projectId,
-              clusterId,
-              porterApp,
-            })}>
-              <Container row>
-                <Icon height="10px" src={refresh} />
-                <Spacer inline width="5px" />
-                Retry
-              </Container>
-            </Link>
-          </Wrapper>
-        );
+        return renderLogsAndRetry(event);
       default:
         return (
           <Wrapper>

+ 17 - 8
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx → dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/BuildEventFocusView.tsx

@@ -3,22 +3,22 @@ import Spacer from "components/porter/Spacer";
 import React, { useEffect, useRef, useState } from "react";
 import api from "shared/api";
 import styled from "styled-components";
-import Anser, { AnserJsonEntry } from "anser";
+import Anser, { type AnserJsonEntry } from "anser";
 import JSZip from "jszip";
 import dayjs from "dayjs";
 import Text from "components/porter/Text";
 import { readableDate } from "shared/string_utils";
-import { getDuration } from "../utils";
+import {getDuration, getStatusColor} from "../utils";
 import Link from "components/porter/Link";
-import { PorterAppBuildEvent } from "../types";
-import { PorterLog } from "main/home/app-dashboard/expanded-app/logs/types";
+import { type PorterAppBuildEvent } from "../types";
+import { type PorterLog } from "main/home/app-dashboard/expanded-app/logs/types";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 
 type Props = {
     event: PorterAppBuildEvent;
 };
 
-const BuildFailureEventFocusView: React.FC<Props> = ({
+const BuildEventFocusView: React.FC<Props> = ({
     event,
 }) => {
     const { porterApp, projectId, clusterId } = useLatestRevision();
@@ -56,7 +56,7 @@ const BuildFailureEventFocusView: React.FC<Props> = ({
                     run_id: event.metadata.action_run_id.toString(),
                 }
             );
-            let logs: PorterLog[] = [];
+            const logs: PorterLog[] = [];
             if (res.data != null) {
                 // Fetch the logs
                 const logsResponse = await fetch(res.data);
@@ -121,9 +121,18 @@ const BuildFailureEventFocusView: React.FC<Props> = ({
         getBuildLogs();
     }, []);
 
+    const renderHeaderText = () => {
+        switch (event.status) {
+            case "CANCELED":
+                return <Text color={getStatusColor(event.status)} size={16}>Build canceled</Text>;
+            case "FAILED":
+                return <Text color={getStatusColor(event.status)} size={16}>Build failed</Text>;
+        }
+    };
+
     return (
         <>
-            <Text size={16} color="#FF6060">Build failed</Text>
+            {renderHeaderText()}
             <Spacer y={0.5} />
             <Text color="helper">Started {readableDate(event.created_at)} and ran for {getDuration(event)}.</Text>
             <Spacer y={0.5} />
@@ -186,7 +195,7 @@ const BuildFailureEventFocusView: React.FC<Props> = ({
     );
 };
 
-export default BuildFailureEventFocusView;
+export default BuildEventFocusView;
 
 const StyledLogsSection = styled.div`
   width: 100%;

+ 4 - 4
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/EventFocusView.tsx

@@ -4,10 +4,10 @@ import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import styled from "styled-components";
 import Link from "components/porter/Link";
-import BuildFailureEventFocusView from "./BuildFailureEventFocusView";
+import BuildEventFocusView from "./BuildEventFocusView";
 import PreDeployEventFocusView from "./PredeployEventFocusView";
 import _ from "lodash";
-import { PorterAppBuildEvent, PorterAppPreDeployEvent, porterAppEventValidator } from "../types";
+import { type PorterAppBuildEvent, type PorterAppPreDeployEvent, porterAppEventValidator } from "../types";
 import { useLocation } from "react-router";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 import { useQuery } from "@tanstack/react-query";
@@ -50,7 +50,7 @@ const EventFocusView: React.FC = ({ }) => {
         },
         {
             // last condition checks if the event is done running; then we stop refetching
-            enabled: eventId != null && eventId !== "" && !(event != null && event.metadata.end_time != null),
+            enabled: eventId != null && eventId !== "" && !(event?.metadata.end_time != null),
             refetchInterval: EVENT_POLL_INTERVAL,
         }
     );
@@ -63,7 +63,7 @@ const EventFocusView: React.FC = ({ }) => {
 
     const getEventFocusView = () => {
         return match(event)
-            .with({ type: "BUILD" }, (ev) => <BuildFailureEventFocusView event={ev} />)
+            .with({ type: "BUILD" }, (ev) => <BuildEventFocusView event={ev} />)
             .with({ type: "PRE_DEPLOY" }, (ev) => <PreDeployEventFocusView event={ev} />)
             .with(null, () => {
                 if (eventId != null && eventId !== "") {