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

Merge pull request #1459 from porter-dev/belanger/por-160-pod-events-backend

Query for pod status -> master
abelanger5 4 лет назад
Родитель
Сommit
32d32c3545

+ 73 - 0
api/server/handlers/namespace/get_pod.go

@@ -0,0 +1,73 @@
+package namespace
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"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/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type GetPodHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetPodHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetPodHandler {
+	return &GetPodHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *GetPodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	agent, err := c.GetAgent(r, cluster, "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	name, err := requestutils.GetURLParamString(r, types.URLParamPodName)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	namespace, err := requestutils.GetURLParamString(r, types.URLParamNamespace)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	pod, err := agent.GetPodByName(name, namespace)
+
+	if errors.Is(err, kubernetes.IsNotFoundError) {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("pod %s/%s was not found", namespace, name),
+			http.StatusNotFound,
+		))
+
+		return
+	} else if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, pod)
+}

+ 33 - 0
api/server/router/namespace.go

@@ -425,6 +425,39 @@ func getNamespaceRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/pods/{name} -> namespace.NewGetPodHandler
+	getPodEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/pods/{%s}",
+					relPath,
+					types.URLParamPodName,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+			},
+		},
+	)
+
+	getPodHandler := namespace.NewGetPodHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getPodEndpoint,
+		Handler:  getPodHandler,
+		Router:   r,
+	})
+
 	// DELETE /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/pods/{name} -> namespace.NewDeletePodHandler
 	deletePodEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 106 - 47
dashboard/src/components/events/SubEventsList.tsx

@@ -1,11 +1,11 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
-import backArrow from "assets/back_arrow.png";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import SubEventCard from "./sub-events/SubEventCard";
 import Loading from "components/Loading";
 import LogBucketCard from "./sub-events/LogBucketCard";
+import useLastSeenPodStatus from "./useLastSeenPodStatus";
 
 const getReadableDate = (s: number) => {
   let ts = new Date(s);
@@ -20,8 +20,18 @@ const getReadableDate = (s: number) => {
 const SubEventsList: React.FC<{
   clearSelectedEvent: () => void;
   event: any;
-}> = ({ event, clearSelectedEvent }) => {
+  enableTopMargin?: boolean;
+}> = ({ event, clearSelectedEvent, enableTopMargin }) => {
   const { currentProject, currentCluster } = useContext(Context);
+  const {
+    status,
+    hasError: hasPodStatusErrored,
+    isLoading: isPodStatusLoading,
+  } = useLastSeenPodStatus({
+    podName: event.name,
+    namespace: event.namespace,
+    resource_type: event.resource_type,
+  });
   const [isLoading, setIsLoading] = useState(true);
   const [subEvents, setSubEvents] = useState(null);
 
@@ -104,28 +114,64 @@ const SubEventsList: React.FC<{
   }, [subEvents]);
 
   return (
-    <Timeline>
-      <ControlRow>
-        <BackButton onClick={clearSelectedEvent}>
-          <i className="material-icons">close</i>
-        </BackButton>
-        <Icon
-          status={event.event_type.toLowerCase() as any}
-          className="material-icons-outlined"
-        >
-          {event.event_type === "critical" ? "report_problem" : "info"}
-        </Icon>
-        Pod {event.name} crashed
-      </ControlRow>
-      {isLoading ? (
-        <Placeholder>
-          <Loading />
-        </Placeholder>
-      ) : sortedSubEvents?.length ? (
-        <EventsGrid>
-          <Rail />
-          {sortedSubEvents.map((subEvent: any, i: number) => {
-            if (subEvent?.event_type === "log_bucket") {
+    <>
+      <Timeline enableTopMargin={enableTopMargin}>
+        <ControlRow>
+          <BackButton onClick={clearSelectedEvent}>
+            <i className="material-icons">close</i>
+          </BackButton>
+          <Icon
+            status={event.event_type.toLowerCase() as any}
+            className="material-icons-outlined"
+          >
+            {event.event_type === "critical" ? "report_problem" : "info"}
+          </Icon>
+          <div>
+            Pod {event.name} crashed
+            {event?.resource_type?.toLowerCase() === "pod" && (
+              <StyledHelper>
+                {hasPodStatusErrored ? (
+                  "We couldn't retrieve last pod status, please try again later"
+                ) : (
+                  <>
+                    {isPodStatusLoading ? (
+                      "Loading last seen pod status"
+                    ) : (
+                      <>
+                        Last seen pod status: {status}{" "}
+                        <StatusColor
+                          status={status?.toLowerCase()}
+                        ></StatusColor>
+                      </>
+                    )}
+                  </>
+                )}
+              </StyledHelper>
+            )}
+          </div>
+        </ControlRow>
+        {isLoading ? (
+          <Placeholder>
+            <Loading />
+          </Placeholder>
+        ) : sortedSubEvents?.length ? (
+          <EventsGrid>
+            <Rail />
+            {sortedSubEvents.map((subEvent: any, i: number) => {
+              if (subEvent?.event_type === "log_bucket") {
+                return (
+                  <Wrapper>
+                    <TimelineNode>
+                      <Penumbra>
+                        <Circle />
+                      </Penumbra>
+                      {getReadableDate(subEvent.timestamp)}
+                    </TimelineNode>
+                    <LogBucketCard logEvent={subEvent} />
+                    {i === sortedSubEvents.length - 1 && <RailCover />}
+                  </Wrapper>
+                );
+              }
               return (
                 <Wrapper>
                   <TimelineNode>
@@ -134,37 +180,31 @@ const SubEventsList: React.FC<{
                     </Penumbra>
                     {getReadableDate(subEvent.timestamp)}
                   </TimelineNode>
-                  <LogBucketCard logEvent={subEvent} />
+                  <SubEventCard subEvent={subEvent} />
                   {i === sortedSubEvents.length - 1 && <RailCover />}
                 </Wrapper>
               );
-            }
-            return (
-              <Wrapper>
-                <TimelineNode>
-                  <Penumbra>
-                    <Circle />
-                  </Penumbra>
-                  {getReadableDate(subEvent.timestamp)}
-                </TimelineNode>
-                <SubEventCard subEvent={subEvent} />
-                {i === sortedSubEvents.length - 1 && <RailCover />}
-              </Wrapper>
-            );
-          })}
-        </EventsGrid>
-      ) : (
-        <Placeholder>
-          <i className="material-icons">search</i>
-          No sub-events were found.
-        </Placeholder>
-      )}
-    </Timeline>
+            })}
+          </EventsGrid>
+        ) : (
+          <Placeholder>
+            <i className="material-icons">search</i>
+            No sub-events were found.
+          </Placeholder>
+        )}
+      </Timeline>
+    </>
   );
 };
 
 export default SubEventsList;
 
+const StyledHelper = styled.div`
+  color: #aaaabb;
+  line-height: 1.6em;
+  font-size: 13px;
+`;
+
 const Placeholder = styled.div`
   padding: 30px;
   padding-bottom: 40px;
@@ -240,6 +280,8 @@ const Rail = styled.div`
 `;
 
 const Timeline = styled.div`
+  margin-top: ${(props: { enableTopMargin: boolean }) =>
+    props.enableTopMargin ? "30px" : "unset"};
   animation: floatIn 0.3s;
   animation-timing-function: ease-out;
   animation-fill-mode: forwards;
@@ -299,3 +341,20 @@ const EventsGrid = styled.div`
   position: relative;
   padding-top: 9px;
 `;
+
+const StatusColor = styled.div`
+  display: inline-block;
+  margin-right: 7px;
+  width: 7px;
+  min-width: 7px;
+  height: 7px;
+  background: ${(props: { status: string }) =>
+    props.status === "running"
+      ? "#4797ff"
+      : props.status === "failed" || props.status === "deleted"
+      ? "#ed5f85"
+      : props.status === "completed"
+      ? "#00d12a"
+      : "#f5cb42"};
+  border-radius: 20px;
+`;

+ 1 - 1
dashboard/src/components/events/sub-events/SubEventCard.tsx

@@ -35,7 +35,7 @@ const StyledCard = styled.div<{ status: string }>`
   padding: 14px;
   padding-left: 13px;
   overflow: hidden;
-  height: 55px;
+  min-height: 55px;
   font-size: 13px;
   color: #aaaabb;
   animation: fadeIn 0.5s;

+ 89 - 0
dashboard/src/components/events/useLastSeenPodStatus.ts

@@ -0,0 +1,89 @@
+import { useContext, useEffect, useState } from "react";
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+const useLastSeenPodStatus = ({
+  podName,
+  namespace,
+  resource_type,
+}: {
+  podName: string;
+  namespace: string;
+  resource_type: string;
+}) => {
+  const [status, setCurrentStatus] = useState(null);
+  const [isLoading, setIsLoading] = useState(true);
+  const [hasError, setHasError] = useState(false);
+  const { currentProject, currentCluster } = useContext(Context);
+
+  const getPodStatus = (status: any) => {
+    if (
+      status?.phase === "Pending" &&
+      status?.containerStatuses !== undefined
+    ) {
+      return status.containerStatuses[0].state.waiting.reason;
+    } else if (status?.phase === "Pending") {
+      return "Pending";
+    }
+
+    if (status?.phase === "Failed") {
+      return "failed";
+    }
+
+    if (status?.phase === "Running") {
+      let collatedStatus = "running";
+
+      status?.containerStatuses?.forEach((s: any) => {
+        if (s.state?.waiting) {
+          collatedStatus =
+            s.state?.waiting.reason === "CrashLoopBackOff"
+              ? "failed"
+              : "waiting";
+        } else if (s.state?.terminated) {
+          collatedStatus = "failed";
+        }
+      });
+      return collatedStatus;
+    }
+  };
+
+  const updatePods = async () => {
+    try {
+      const res = await api.getPodByName(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          namespace: "default",
+          name: podName,
+        }
+      );
+      console.log(getPodStatus(res.data.status));
+
+      setCurrentStatus(getPodStatus(res.data.status));
+    } catch (error) {
+      if (error?.response?.status === 404) {
+        setCurrentStatus("Deleted");
+      } else {
+        setHasError(true);
+      }
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (resource_type?.toLowerCase() === "pod") {
+      updatePods();
+    }
+  }, [podName, namespace, resource_type]);
+
+  return {
+    status,
+    isLoading,
+    hasError,
+  };
+};
+
+export default useLastSeenPodStatus;

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/dashboard/events/EventsTab.tsx

@@ -54,6 +54,7 @@ const EventsTab = () => {
       <SubEventsList
         event={currentEvent}
         clearSelectedEvent={() => setCurrentEvent(null)}
+        enableTopMargin
       />
     );
   }

+ 3 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -65,7 +65,9 @@ const StatusSectionFC: React.FunctionComponent<Props> = ({
         setControllers([]);
         setIsLoading(false);
       });
-    return () => (isSubscribed = false);
+    return () => {
+      isSubscribed = false;
+    };
   }, [currentProject, currentCluster, setCurrentError, currentChart]);
 
   const renderLogs = () => {

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

@@ -635,6 +635,20 @@ const getJobPods = baseApi<
   return `/api/projects/${id}/clusters/${cluster_id}/namespaces/${namespace}/jobs/${name}/pods`;
 });
 
+const getPodByName = baseApi<
+  {},
+  {
+    project_id: number;
+    cluster_id: number;
+    namespace: string;
+    name: string;
+  }
+>(
+  "GET",
+  ({ project_id, cluster_id, namespace, name }) =>
+    `/api/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/pods/${name}`
+);
+
 const getMatchingPods = baseApi<
   {
     namespace: string;
@@ -1254,6 +1268,7 @@ export default {
   getJobs,
   getJobStatus,
   getJobPods,
+  getPodByName,
   getMatchingPods,
   getMetrics,
   getNamespaces,

+ 20 - 0
internal/kubernetes/agent.go

@@ -518,6 +518,26 @@ func (a *Agent) GetPodsByLabel(selector string, namespace string) (*v1.PodList,
 	)
 }
 
+// GetPodByName retrieves a single instance of pod with given name
+func (a *Agent) GetPodByName(name string, namespace string) (*v1.Pod, error) {
+	// Get pod by name
+	pod, err := a.Clientset.CoreV1().Pods(namespace).Get(
+		context.TODO(),
+		name,
+		metav1.GetOptions{},
+	)
+
+	if err != nil && errors.IsNotFound(err) {
+		return nil, IsNotFoundError
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	return pod, nil
+}
+
 // DeletePod deletes a pod by name and namespace
 func (a *Agent) DeletePod(namespace string, name string) error {
 	err := a.Clientset.CoreV1().Pods(namespace).Delete(