Просмотр исходного кода

Ingest pre-deploy events from the DB and get logs associated with their runs (#3097)

Feroze Mohideen 2 лет назад
Родитель
Сommit
3f57e9a213

+ 6 - 1
api/server/handlers/cluster/get_logs_pod_values.go

@@ -38,7 +38,12 @@ func (c *GetLogPodValuesHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	agent, err := c.GetAgent(r, cluster, "")
+	var namespace string
+	if request.Namespace != "" {
+		namespace = request.Namespace
+	}
+
+	agent, err := c.GetAgent(r, cluster, namespace)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

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

@@ -45,7 +45,7 @@ const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData }) => {
         }
       );
       setNumPages(res.data.num_pages);
-      setEvents((res.data.events as PorterAppEvent[]).filter(e => e.type === PorterAppEventType.BUILD));
+      setEvents((res.data.events as PorterAppEvent[]).filter(e => e.type === PorterAppEventType.BUILD || e.type === PorterAppEventType.PRE_DEPLOY));
       setLoading(false);
     } catch (err) {
       setError(err);
@@ -100,7 +100,7 @@ const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData }) => {
               <Spacer x={0.5} />
               <Text>{feedDate(event.created_at).split(", ")[1]}</Text>
             </Time>
-            <EventCard appData={appData} event={event} i={i} />
+            <EventCard appData={appData} event={event} key={i} />
           </EventWrapper>
         );
       })}

+ 11 - 35
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/BuildEventCard.tsx

@@ -18,7 +18,7 @@ import JSZip from "jszip";
 import Anser, { AnserJsonEntry } from "anser";
 import GHALogsModal from "../../status/GHALogsModal";
 import { PorterAppEvent, PorterAppEventType } from "shared/types";
-import { getDuration, getStatusIcon } from './utils';
+import { getDuration, getStatusIcon, triggerWorkflow } from './utils';
 import { StyledEventCard } from "./EventCard";
 
 type Props = {
@@ -42,28 +42,7 @@ const BuildEventCard: React.FC<Props> = ({ event, appData }) => {
         return <Text color="#aaaabb66">Build in progress...</Text>;
     }
   };
-  const triggerWorkflow = async () => {
-    try {
-      const res = await api.reRunGHWorkflow(
-        "",
-        {},
-        {
-          project_id: appData.app.project_id,
-          cluster_id: appData.app.cluster_id,
-          git_installation_id: appData.app.git_repo_id,
-          owner: appData.app.repo_name?.split("/")[0],
-          name: appData.app.repo_name?.split("/")[1],
-          branch: appData.app.branch_name,
-          filename: "porter_stack_" + appData.chart.name + ".yml",
-        }
-      );
-      if (res.data != null) {
-        window.open(res.data, "_blank", "noreferrer");
-      }
-    } catch (error) {
-      console.log(error);
-    }
-  };
+
   const getBuildLogs = async () => {
     try {
       setLogs([]);
@@ -138,7 +117,7 @@ const BuildEventCard: React.FC<Props> = ({ event, appData }) => {
     }
   };
 
-  const renderInfoCta = (event: any) => {
+  const renderInfoCta = (event: PorterAppEvent) => {
     switch (event.status) {
       case "SUCCESS":
         return (
@@ -176,6 +155,14 @@ const BuildEventCard: React.FC<Props> = ({ event, appData }) => {
               />
             )}
             <Spacer inline x={1} />
+
+            <Link hasunderline onClick={() => triggerWorkflow(appData)}>
+              <Container row>
+                <Icon height="10px" src={refresh} />
+                <Spacer inline width="5px" />
+                Retry
+              </Container>
+            </Link>
           </>
         );
       default:
@@ -216,17 +203,6 @@ const BuildEventCard: React.FC<Props> = ({ event, appData }) => {
           {renderStatusText(event)}
           <Spacer inline x={1} />
           {renderInfoCta(event)}
-          {event.status === "FAILED" && (
-            <>
-              <Link hasunderline onClick={() => triggerWorkflow()}>
-                <Container row>
-                  <Icon height="10px" src={refresh} />
-                  <Spacer inline width="5px" />
-                  Retry
-                </Container>
-              </Link>
-            </>
-          )}
         </Container>
       </Container>
       {showModal && (

+ 1 - 2
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/EventCard.tsx

@@ -25,8 +25,7 @@ const EventCard: React.FC<Props> = ({ event, appData }) => {
       // TODO: implement
       // return <DeployEventCard event={event} appData={appData} />;
       case PorterAppEventType.PRE_DEPLOY:
-      // TODO: implement
-      // return <PreDeployEventCard event={event} />;
+        return <PreDeployEventCard event={event} appData={appData} />;
       default:
         return null;
     };

+ 86 - 58
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/PreDeployEventCard.tsx

@@ -1,8 +1,9 @@
-import React, { useEffect, useState } from "react";
+import React, { useState } from "react";
 
 import pre_deploy from "assets/pre_deploy.png";
 
 import run_for from "assets/run_for.png";
+import refresh from "assets/refresh.png";
 
 import Text from "components/porter/Text";
 import Container from "components/porter/Container";
@@ -11,16 +12,23 @@ import Icon from "components/porter/Icon";
 import Modal from "components/porter/Modal";
 
 import { PorterAppEvent } from "shared/types";
-import { getDuration, getStatusIcon } from './utils';
+import { getDuration, getStatusIcon, triggerWorkflow } from './utils';
 import { StyledEventCard } from "./EventCard";
+import Link from "components/porter/Link";
+import LogsModal from "../../status/LogsModal";
+import api from "shared/api";
+import dayjs from "dayjs";
 
 type Props = {
   event: PorterAppEvent;
+  appData: any;
 };
 
-const PreDeployEventCard: React.FC<Props> = ({ event }) => {
+const PreDeployEventCard: React.FC<Props> = ({ event, appData }) => {
   const [showModal, setShowModal] = useState<boolean>(false);
   const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+  const [logModalVisible, setLogModalVisible] = useState(false);
+  const [pods, setPods] = useState([]);
 
   const renderStatusText = (event: PorterAppEvent) => {
     switch (event.status) {
@@ -33,59 +41,66 @@ const PreDeployEventCard: React.FC<Props> = ({ event }) => {
     }
   };
 
-  const renderInfoCta = (event: any) => {
-    switch (event.status) {
-      case "SUCCESS":
-        return (
-          <>
-            {/* <Link hasunderline onClick={() => getBuildLogs()}>
-              View logs
-            </Link>
-
-            {logModalVisible && (
-              <GHALogsModal
-                appData={appData}
-                logs={logs}
-                modalVisible={logModalVisible}
-                setModalVisible={setLogModalVisible}
-                actionRunId={event.metadata?.action_run_id}
-              />
-            )}
-            <Spacer inline x={1} /> */}
-          </>
-        );
-      case "FAILED":
-        return (
-          <>
-            {/* <Link hasunderline onClick={() => getBuildLogs()}>
-              View logs
-            </Link>
-
-            {logModalVisible && (
-              <GHALogsModal
-                appData={appData}
-                logs={logs}
-                modalVisible={logModalVisible}
-                setModalVisible={setLogModalVisible}
-                actionRunId={event.metadata?.action_run_id}
-              />
-            )}
-            <Spacer inline x={1} /> */}
-          </>
-        );
-      default:
+  const getPodWithCorrectTimestamp = (
+    getJobPodsResponses: any[],
+    start_range: dayjs.Dayjs,
+    end_range: dayjs.Dayjs,
+  ) => {
+    let filteredObjects = getJobPodsResponses.filter(obj =>
+      obj.data &&
+      obj.data.some((d: any) => {
         return (
-          <>
-            {/* <Link
-              hasunderline
-              target="_blank"
-              to={`https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata?.action_run_id}`}
-            >
-              View live logs
-            </Link>
-            <Spacer inline x={1} /> */}
-          </>
+          d?.metadata?.creationTimestamp &&
+          dayjs(d.metadata.creationTimestamp) >= start_range &&
+          dayjs(d.metadata.creationTimestamp) <= end_range
         );
+      })
+    );
+
+    if (filteredObjects.length === 0) {
+      return undefined;
+    }
+
+    return filteredObjects[filteredObjects.length - 1]
+  }
+
+  const getPredeployLogs = async () => {
+    setLogModalVisible(true);
+    try {
+      // get the pod name
+      const filters = {
+        namespace: appData.releaseChart.namespace,
+        match_prefix: appData.releaseChart.name,
+        start_range: dayjs(event.metadata.start_time).toISOString(),
+        end_range: dayjs(event.metadata.end_time).toISOString(),
+      };
+      const logPodValuesResp = await api.getLogPodValues("<TOKEN>", filters, {
+        project_id: appData.app.project_id,
+        cluster_id: appData.app.cluster_id,
+      });
+      const logPodValues = logPodValuesResp.data;
+
+      if (logPodValues != null && logPodValues.length > 0) {
+        // wheeeee
+        const podNames = logPodValues.map((v: string) => v.split('-hook')[0] + '-hook');
+        const getJobPodsResponses = await Promise.all(podNames.map((podName: string) => api.getJobPods(
+          "<token>",
+          {},
+          {
+            id: appData.app.project_id,
+            name: podName,
+            cluster_id: appData.app.cluster_id,
+            namespace: appData.releaseChart.namespace,
+          },
+        )));
+        const latestPod = getPodWithCorrectTimestamp(getJobPodsResponses, dayjs(event.metadata.start_time), dayjs(event.metadata.end_time));
+        if (latestPod != null) {
+          setPods(latestPod.data);
+        }
+      }
+
+    } catch (error) {
+      console.log(error);
     }
   };
 
@@ -110,10 +125,14 @@ const PreDeployEventCard: React.FC<Props> = ({ event }) => {
           <Spacer inline width="10px" />
           {renderStatusText(event)}
           <Spacer inline x={1} />
-          {renderInfoCta(event)}
-          {/* {event.status === "FAILED" && (
+          <Link hasunderline onClick={() => getPredeployLogs()}>
+            View logs
+          </Link>
+          {event.status === "FAILED" && (
             <>
-              <Link hasunderline onClick={() => triggerWorkflow()}>
+              <Spacer inline x={1} />
+
+              <Link hasunderline onClick={() => triggerWorkflow(appData)}>
                 <Container row>
                   <Icon height="10px" src={refresh} />
                   <Spacer inline width="5px" />
@@ -121,7 +140,16 @@ const PreDeployEventCard: React.FC<Props> = ({ event }) => {
                 </Container>
               </Link>
             </>
-          )} */}
+          )}
+          {logModalVisible && (
+            <LogsModal
+              selectedPod={pods[0]}
+              podError={!pods[0] ? "Pod no longer exists." : ""}
+              setModalVisible={setLogModalVisible}
+              logsName={"pre-deploy"}
+            />
+          )}
+          <Spacer inline x={1} />
         </Container>
       </Container>
       {showModal && (

+ 24 - 0
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts

@@ -2,6 +2,7 @@ import { PorterAppEvent } from "shared/types";
 import healthy from "assets/status-healthy.png";
 import failure from "assets/failure.png";
 import loading from "assets/loading.gif";
+import api from "shared/api";
 
 export const getDuration = (event: PorterAppEvent): string => {
     const startTimeStamp = new Date(event.created_at).getTime();
@@ -42,4 +43,27 @@ export const getStatusIcon = (status: string) => {
         default:
             return loading;
     }
+};
+
+export const triggerWorkflow = async (appData: any) => {
+    try {
+        const res = await api.reRunGHWorkflow(
+            "",
+            {},
+            {
+                project_id: appData.app.project_id,
+                cluster_id: appData.app.cluster_id,
+                git_installation_id: appData.app.git_repo_id,
+                owner: appData.app.repo_name?.split("/")[0],
+                name: appData.app.repo_name?.split("/")[1],
+                branch: appData.app.branch_name,
+                filename: "porter_stack_" + appData.chart.name + ".yml",
+            }
+        );
+        if (res.data != null) {
+            window.open(res.data, "_blank", "noreferrer");
+        }
+    } catch (error) {
+        console.log(error);
+    }
 };

+ 1 - 1
dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJobRun.tsx

@@ -253,7 +253,7 @@ const ExpandedJobRun = ({
         </DeprecatedWarning>
         <LogsSection
           isFullscreen={false}
-          setIsFullscreen={() => {}}
+          setIsFullscreen={() => { }}
           overridingPodName={pods[0]?.metadata?.name || jobRun.metadata?.name}
           currentChart={currentChart}
           initData={initData}

+ 155 - 0
dashboard/src/main/home/app-dashboard/expanded-app/status/ExpandedIncidentLogs.tsx

@@ -0,0 +1,155 @@
+import { useEffect, useRef, useState } from "react";
+import { Log } from "../useAgentLogs";
+import React from "react";
+import styled from "styled-components";
+import Loading from "components/Loading";
+import dayjs from "dayjs";
+import Anser from "anser";
+
+interface ExpandedIncidentLogsProps {
+    logs: Log[];
+}
+
+const ExpandedIncidentLogs: React.FC<ExpandedIncidentLogsProps> = ({ logs }: ExpandedIncidentLogsProps) => {
+    const scrollToBottomRef = useRef<HTMLDivElement>(null);
+
+    useEffect(() => {
+        if (scrollToBottomRef.current) {
+            scrollToBottomRef.current.scrollIntoView({
+                behavior: "smooth",
+                block: "end",
+            });
+        }
+    }, [logs, scrollToBottomRef]);
+
+    return logs.length ?
+        (<LogsSectionWrapper>
+            <StyledLogsSection>
+                {logs?.map((log, i) => {
+                    return (
+                        <LogSpan key={[log.lineNumber, i].join(".")}>
+                            <span className="line-number">{log.lineNumber}.</span>
+                            {log.timestamp && <span className="line-timestamp">
+                                {dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")}
+                            </span>}
+                            <LogOuter key={[log.lineNumber, i].join(".")}>
+                                {log.line?.map((ansi, j) => {
+                                    if (ansi.clearLine) {
+                                        return null;
+                                    }
+
+                                    return (
+                                        <LogInnerSpan
+                                            key={[log.lineNumber, i, j].join(".")}
+                                            ansi={ansi}
+                                        >
+                                            {ansi.content.replace(/ /g, "\u00a0")}
+                                        </LogInnerSpan>
+                                    );
+                                })}
+                            </LogOuter>
+                        </LogSpan>
+                    );
+                })}
+                <div ref={scrollToBottomRef} />
+            </StyledLogsSection>
+        </LogsSectionWrapper>)
+        :
+        (<LogsLoadWrapper>
+            <Loading />
+        </LogsLoadWrapper >)
+};
+
+export default ExpandedIncidentLogs;
+
+
+const LogsSectionWrapper = styled.div`
+  position: relative;
+`;
+
+const StyledLogsSection = styled.div`
+  margin-top: 20px;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  font-size: 13px;
+  max-height: 400px;
+  border-radius: 8px;
+  border: 1px solid #ffffff33;
+  border-top: none;
+  background: #101420;
+  animation: floatIn 0.3s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  overflow-y: auto;
+  overflow-wrap: break-word;
+  position: relative;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;
+
+const LogSpan = styled.div`
+  font-family: monospace;
+  user-select: text;
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  width: 100%;
+  & > * {
+    padding-block: 5px;
+  }
+  & > .line-timestamp {
+    height: 100%;
+    color: #949effff;
+    opacity: 0.5;
+    font-family: monospace;
+    min-width: fit-content;
+    padding-inline-end: 5px;
+  }
+  & > .line-number {
+    height: 100%;
+    background: #202538;
+    display: inline-block;
+    text-align: right;
+    min-width: 45px;
+    padding-inline-end: 5px;
+    opacity: 0.3;
+    font-family: monospace;
+  }
+`;
+
+const LogOuter = styled.div`
+  display: inline-block;
+  word-wrap: anywhere;
+  flex-grow: 1;
+  font-family: monospace, sans-serif;
+  font-size: 12px;
+`;
+
+const LogInnerSpan = styled.span`
+  font-family: monospace, sans-serif;
+  font-size: 12px;
+  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
+        props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
+  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
+        props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
+  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
+        props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
+`;
+
+export const ViewLogsWrapper = styled.div`
+  margin-bottom: -15px;
+  margin-top: 15px;
+`;
+const LogsLoadWrapper = styled.div`
+  height: 50px;
+`;

+ 1 - 1
dashboard/src/main/home/app-dashboard/expanded-app/status/GHALogsModal.tsx

@@ -32,7 +32,7 @@ const GHALogsModal: React.FC<Props> = ({
   actionRunId,
 }) => {
   const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
-  const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
+  const scrollToBottomRef = useRef<HTMLDivElement>(null);
   const ExpandedIncidentLogs = ({ logs }: ExpandedIncidentLogsProps) => {
     if (!logs.length) {
       return (

+ 57 - 0
dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx

@@ -0,0 +1,57 @@
+import React, { useEffect, useRef } from "react";
+import Modal from "components/porter/Modal";
+import TitleSection from "components/TitleSection";
+import { Log } from "../useAgentLogs";
+import Text from "components/porter/Text";
+import danger from "assets/danger.svg";
+
+import ExpandedIncidentLogs from "./ExpandedIncidentLogs";
+import { SelectedPodType } from "./types";
+import { useLogs } from "./useLogs";
+
+interface LogsModalProps {
+    selectedPod: SelectedPodType;
+    podError: string;
+    logsName: string;
+    setModalVisible: (x: boolean) => void;
+}
+const LogsModal: React.FC<LogsModalProps> = ({ selectedPod, setModalVisible, logsName }) => {
+    const scrollToBottomRef = useRef<HTMLDivElement>(null);
+    const scrollToBottom = () => {
+        if (scrollToBottomRef.current) {
+            scrollToBottomRef.current.scrollIntoView({
+                behavior: "smooth",
+                block: "end",
+            });
+        }
+    }
+    useEffect(() => {
+        scrollToBottom();
+    }, [scrollToBottomRef]);
+
+    const { logs } = useLogs(selectedPod, scrollToBottom);
+
+    const renderLogs = (): Log[] => {
+        if (!Array.isArray(logs) || logs?.length === 0) {
+            return (
+                []
+            );
+        }
+
+        return logs.map((log, i) => ({
+            line: log,
+            lineNumber: i + 1,
+        }));
+    };
+
+    return (
+        <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
+            <TitleSection icon={danger}>
+                <Text size={16}>Logs for {logsName}</Text>
+            </TitleSection>
+            <ExpandedIncidentLogs logs={renderLogs()} />
+        </Modal>
+    );
+};
+
+export default LogsModal;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx

@@ -250,7 +250,7 @@ const ExpandedJobRun = ({
         </DeprecatedWarning>
         <LogsSection
           isFullscreen={false}
-          setIsFullscreen={() => {}}
+          setIsFullscreen={() => { }}
           overridingPodName={pods[0]?.metadata?.name || jobRun.metadata?.name}
           currentChart={currentChart}
           initData={initData}