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

Merge branch '0.8.0-pod-events-managment' of https://github.com/porter-dev/porter into 0.8.0-pod-events-managment

merge remote
Alexander Belanger 4 лет назад
Родитель
Сommit
8fbd9d61b4

+ 35 - 0
.github/workflows/release.yaml

@@ -347,3 +347,38 @@ jobs:
           asset_path: ./release/static/static_${{steps.tag_name.outputs.tag}}.zip
           asset_name: static_${{steps.tag_name.outputs.tag}}.zip
           asset_content_type: application/zip
+  build-push-docker-cli:
+    name: Build a new porter-cli docker image
+    runs-on: ubuntu-latest
+    needs: release
+    steps:
+      - name: Get tag name
+        id: tag_name
+        run: |
+          tag=${GITHUB_TAG/refs\/tags\//}
+          echo ::set-output name=tag::$tag
+        env:
+          GITHUB_TAG: ${{ github.ref }}
+      - name: Checkout
+        uses: actions/checkout@v2.3.4
+      - name: Configure AWS credentials
+        uses: aws-actions/configure-aws-credentials@v1
+        with:
+          aws-access-key-id: ${{ secrets.ECR_AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: ${{ secrets.ECR_AWS_SECRET_ACCESS_KEY }}
+          aws-region: us-east-2
+      - name: Login to ECR public
+        id: login-ecr
+        run: |
+          aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/o1j4x7p4
+      - name: Build
+        run: |
+          docker build ./services/porter_cli_container \
+            -t public.ecr.aws/o1j4x7p4/porter-cli:${{steps.tag_name.outputs.tag}} \
+            -t public.ecr.aws/o1j4x7p4/porter-cli:latest \
+            -f ./services/porter_cli_container/Dockerfile \
+            --build-arg VERSION=${{steps.tag_name.outputs.tag}}
+      - name: Push
+        run: |
+          docker push public.ecr.aws/o1j4x7p4/porter-cli:${{steps.tag_name.outputs.tag}}
+          docker push public.ecr.aws/o1j4x7p4/porter-cli:latest

+ 1 - 1
cli/cmd/api/git_repo.go

@@ -10,7 +10,7 @@ import (
 )
 
 // ListGitRepoResponse is the list of Git repo integrations for a project
-type ListGitRepoResponse []models.GitRepoExternal
+type ListGitRepoResponse []uint
 
 // ListGitRepos returns a list of Git repos for a project
 func (c *Client) ListGitRepos(

+ 0 - 20
cli/cmd/connect.go

@@ -67,18 +67,6 @@ var connectRegistryCmd = &cobra.Command{
 	},
 }
 
-var connectActionsCmd = &cobra.Command{
-	Use:   "actions",
-	Short: "Adds Github Actions to a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runConnectActions)
-
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
 var connectGCRCmd = &cobra.Command{
 	Use:   "gcr",
 	Short: "Adds a GCR instance to a project",
@@ -135,7 +123,6 @@ func init() {
 		"the context to connect (defaults to the current context)",
 	)
 
-	connectCmd.AddCommand(connectActionsCmd)
 	connectCmd.AddCommand(connectECRCmd)
 	connectCmd.AddCommand(connectRegistryCmd)
 	connectCmd.AddCommand(connectDockerhubCmd)
@@ -243,10 +230,3 @@ func runConnectHelmRepoBasic(_ *api.AuthCheckResponse, client *api.Client, _ []s
 
 	return config.SetHelmRepo(hrID)
 }
-
-func runConnectActions(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
-	return connect.Actions(
-		client,
-		config.Project,
-	)
-}

+ 0 - 125
cli/cmd/connect/actions.go

@@ -1,125 +0,0 @@
-package connect
-
-import (
-	"context"
-	"fmt"
-	"strconv"
-	"time"
-
-	"github.com/porter-dev/porter/cli/cmd/api"
-	"github.com/porter-dev/porter/cli/cmd/utils"
-
-	ints "github.com/porter-dev/porter/internal/models/integrations"
-)
-
-// Actions creates a github actions integration
-func Actions(
-	client *api.Client,
-	projectID uint,
-) error {
-	// if project ID is 0, ask the user to set the project ID or create a project
-	if projectID == 0 {
-		return fmt.Errorf("no project set, please run porter project set [id]")
-	}
-
-	// list oauth integrations and make sure Github exists
-	oauthInts, err := client.ListOAuthIntegrations(context.TODO(), projectID)
-
-	if err != nil {
-		return err
-	}
-
-	linkedGH := false
-
-	// iterate through oauth integrations to find do
-	for _, oauthInt := range oauthInts {
-		if oauthInt.Client == ints.OAuthGithub {
-			linkedGH = true
-			break
-		}
-	}
-
-	if !linkedGH {
-		_, err = triggerGithubOAuth(client, projectID)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	gitRepos, err := client.ListGitRepos(context.TODO(), projectID)
-
-	gitRepoID := gitRepos[0].ID
-
-	// prompts (unfortunately a lot)
-	clusterIDStr, _ := utils.PromptPlaintext(fmt.Sprintf(`Please provide the cluster id (can be found with "porter clusters list").
-Cluster ID: `))
-	clusterID, err := strconv.ParseUint(clusterIDStr, 10, 64)
-
-	if err != nil {
-		return err
-	}
-
-	releaseName, _ := utils.PromptPlaintext(fmt.Sprintf(`Release name:`))
-	releaseNamespace, _ := utils.PromptPlaintext(fmt.Sprintf(`Release namespace:`))
-	gitRepo, _ := utils.PromptPlaintext(fmt.Sprintf(`Please enter the Github repo, in the form ${owner}/${repo_name}. For example, porter-dev/porter.
-Github repo:`))
-
-	imageRepo, _ := utils.PromptPlaintext(fmt.Sprintf(`Please enter the image repo url.
-Image repo:`))
-
-	dockerfilePath, _ := utils.PromptPlaintext(fmt.Sprintf(`Please enter the path in the repo to your dockerfile.
-Dockerfile path:`))
-
-	err = client.CreateGithubAction(
-		context.Background(),
-		projectID,
-		uint(clusterID),
-		releaseName,
-		releaseNamespace,
-		&api.CreateGithubActionRequest{
-			GitRepo:        gitRepo,
-			ImageRepoURI:   imageRepo,
-			DockerfilePath: dockerfilePath,
-			GitRepoID:      gitRepoID,
-		},
-	)
-
-	return err
-}
-
-func triggerGithubOAuth(client *api.Client, projectID uint) (ints.OAuthIntegrationExternal, error) {
-	var ghAuth ints.OAuthIntegrationExternal
-
-	oauthURL := fmt.Sprintf("%s/oauth/projects/%d/github", client.BaseURL, projectID)
-
-	fmt.Printf("Please visit %s in your browser to connect to Github (it should open automatically).", oauthURL)
-	utils.OpenBrowser(oauthURL)
-
-	for {
-		oauthInts, err := client.ListOAuthIntegrations(context.TODO(), projectID)
-
-		if err != nil {
-			return ghAuth, err
-		}
-
-		linkedGH := false
-
-		// iterate through oauth integrations to find do
-		for _, oauthInt := range oauthInts {
-			if oauthInt.Client == ints.OAuthGithub {
-				linkedGH = true
-				ghAuth = oauthInt
-				break
-			}
-		}
-
-		if linkedGH {
-			break
-		}
-
-		time.Sleep(2 * time.Second)
-	}
-
-	return ghAuth, nil
-}

+ 2 - 2
cli/cmd/deploy/create.go

@@ -59,7 +59,7 @@ func (c *CreateAgent) CreateFromGithub(
 		githubRepos, err := c.Client.ListGithubRepos(
 			context.Background(),
 			c.CreateOpts.ProjectID,
-			gitRepo.ID,
+			gitRepo,
 		)
 
 		if err != nil {
@@ -68,7 +68,7 @@ func (c *CreateAgent) CreateFromGithub(
 
 		for _, githubRepo := range githubRepos {
 			if githubRepo.FullName == ghOpts.Repo {
-				gitRepoMatch = gitRepo.ID
+				gitRepoMatch = gitRepo
 				break
 			}
 		}

+ 6 - 2
cli/cmd/errors.go

@@ -2,12 +2,16 @@ package cmd
 
 import (
 	"context"
+	"errors"
 	"strings"
 
 	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
 )
 
+var ErrNotLoggedIn error = errors.New("You are not logged in.")
+var ErrCannotConnect error = errors.New("Unable to connect to the Porter server.")
+
 func checkLoginAndRun(args []string, runner func(user *api.AuthCheckResponse, client *api.Client, args []string) error) error {
 	client := GetAPIClient(config)
 
@@ -18,12 +22,12 @@ func checkLoginAndRun(args []string, runner func(user *api.AuthCheckResponse, cl
 
 		if strings.Contains(err.Error(), "403") {
 			red.Print("You are not logged in. Log in using \"porter auth login\"\n")
-			return nil
+			return ErrNotLoggedIn
 		} else if strings.Contains(err.Error(), "connection refused") {
 			red.Printf("Unable to connect to the Porter server at %s\n", config.Host)
 			red.Print("To set a different host, run \"porter config set-host [HOST]\"\n")
 			red.Print("To start a local server, run \"porter server start\"\n")
-			return nil
+			return ErrCannotConnect
 		}
 
 		red.Printf("Error: %v\n", err.Error())

+ 1 - 0
cli/cmd/logs.go

@@ -13,6 +13,7 @@ import (
 // without any subcommands
 var logsCmd = &cobra.Command{
 	Use:   "logs [release]",
+	Args:  cobra.ExactArgs(1),
 	Short: "Logs the output from a given application.",
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, logs)

+ 34 - 9
cli/cmd/run.go

@@ -25,6 +25,7 @@ import (
 )
 
 var namespace string
+var verbose bool
 
 // runCmd represents the "porter run" base command when called
 // without any subcommands
@@ -60,6 +61,14 @@ func init() {
 		false,
 		"whether to connect to an existing pod",
 	)
+
+	runCmd.PersistentFlags().BoolVarP(
+		&verbose,
+		"verbose",
+		"v",
+		false,
+		"whether to print verbose output",
+	)
 }
 
 func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
@@ -326,20 +335,25 @@ func executeRunEphemeral(config *PorterRunSharedConfig, namespace, name, contain
 
 		time.Sleep(2 * time.Second)
 
-		// ugly way to catch no TTY errors, such as when running command "echo \"hello\""
-		if i == 4 && err != nil {
-			color.New(color.FgYellow).Println("Could not open a shell to this container. Container logs:\n")
+	}
 
-			var writtenBytes int64
+	// ugly way to catch no TTY errors, such as when running command "echo \"hello\""
+	if err != nil {
+		color.New(color.FgYellow).Println("Could not open a shell to this container. Container logs:\n")
 
-			writtenBytes, err = pipePodLogsToStdout(config, namespace, podName, container, false)
+		var writtenBytes int64
 
-			if writtenBytes == 0 {
-				color.New(color.FgYellow).Println("Could not get logs. Pod events:\n")
+		writtenBytes, err = pipePodLogsToStdout(config, namespace, podName, container, false)
 
-				err = pipeEventsToStdout(config, namespace, podName, container, false)
-			}
+		if verbose || writtenBytes == 0 {
+			color.New(color.FgYellow).Println("Could not get logs. Pod events:\n")
+
+			err = pipeEventsToStdout(config, namespace, podName, container, false)
 		}
+	} else if verbose {
+		color.New(color.FgYellow).Println("Pod events:\n")
+
+		pipeEventsToStdout(config, namespace, podName, container, false)
 	}
 
 	// delete the ephemeral pod
@@ -370,6 +384,9 @@ func pipePodLogsToStdout(config *PorterRunSharedConfig, namespace, name, contain
 }
 
 func pipeEventsToStdout(config *PorterRunSharedConfig, namespace, name, container string, follow bool) error {
+	// update the config in case the operation has taken longer than token expiry time
+	config.setSharedConfig()
+
 	// creates the clientset
 	resp, err := config.Clientset.CoreV1().Events(namespace).List(
 		context.TODO(),
@@ -428,6 +445,9 @@ func createPodFromExisting(config *PorterRunSharedConfig, existing *v1.Pod, args
 
 	newPod.Status = v1.PodStatus{}
 
+	// only use "primary" container
+	newPod.Spec.Containers = newPod.Spec.Containers[0:1]
+
 	// set restart policy to never
 	newPod.Spec.RestartPolicy = v1.RestartPolicyNever
 
@@ -446,6 +466,11 @@ func createPodFromExisting(config *PorterRunSharedConfig, existing *v1.Pod, args
 	newPod.Spec.Containers[0].StdinOnce = true
 	newPod.Spec.NodeName = ""
 
+	// remove health checks and probes
+	newPod.Spec.Containers[0].LivenessProbe = nil
+	newPod.Spec.Containers[0].ReadinessProbe = nil
+	newPod.Spec.Containers[0].StartupProbe = nil
+
 	// create the pod and return it
 	return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
 		context.Background(),

+ 8 - 0
dashboard/package-lock.json

@@ -6244,6 +6244,14 @@
         "scheduler": "^0.19.1"
       }
     },
+    "react-error-boundary": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz",
+      "integrity": "sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA==",
+      "requires": {
+        "@babel/runtime": "^7.12.5"
+      }
+    },
     "react-is": {
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

+ 2 - 1
dashboard/package.json

@@ -45,7 +45,8 @@
     "react-router-dom": "^5.2.0",
     "react-table": "^7.7.0",
     "semver": "^7.3.5",
-    "styled-components": "^5.2.0"
+    "styled-components": "^5.2.0",
+    "react-error-boundary": "^3.1.3"
   },
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",

+ 41 - 3
dashboard/src/App.tsx

@@ -1,14 +1,52 @@
 import React, { Component } from "react";
 import { BrowserRouter } from "react-router-dom";
+import PorterErrorBoundary from "shared/PorterErrorBoundary";
+import styled, { createGlobalStyle } from "styled-components";
 
 import MainWrapper from "./main/MainWrapper";
 
 export default class App extends Component {
   render() {
     return (
-      <BrowserRouter>
-        <MainWrapper />
-      </BrowserRouter>
+      <StyledMain>
+        <GlobalStyle />
+        <PorterErrorBoundary errorBoundaryLocation="globalErrorBoundary">
+          <BrowserRouter>
+            <MainWrapper />
+          </BrowserRouter>
+        </PorterErrorBoundary>
+      </StyledMain>
     );
   }
 }
+
+const GlobalStyle = createGlobalStyle`
+  * {
+    box-sizing: border-box;
+    font-family: 'Work Sans', sans-serif;
+  }
+  
+  body {
+    background: #202227;
+    overscroll-behavior-x: none;
+  }
+
+  a {
+    color: #949eff;
+    text-decoration: none;
+  }
+
+  img {
+    max-width: 100%;
+  }
+`;
+
+const StyledMain = styled.div`
+  height: 100vh;
+  width: 100vw;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #202227;
+  color: white;
+`;

+ 0 - 12
dashboard/src/components/EventLogs.tsx

@@ -1,12 +0,0 @@
-import { Event } from "main/home/cluster-dashboard/expanded-chart/events/EventsTab";
-import React from "react";
-
-type EventLogsProps = {
-  event: Event;
-};
-
-const EventLogs: React.FunctionComponent<EventLogsProps> = ({}) => {
-  return <div>Show logs</div>;
-};
-
-export default EventLogs;

+ 117 - 0
dashboard/src/components/UnexpectedErrorPage.tsx

@@ -0,0 +1,117 @@
+import React from "react";
+import styled from "styled-components";
+
+const UnexpectedErrorPage: React.FC = ({ error, resetErrorBoundary }: any) => (
+  <>
+    <StyledPageNotFound>
+      <Mega>
+        Unknwown
+        <Inside>Unknown Error</Inside>
+      </Mega>
+      <Flex>
+        <BackButton width="140px" onClick={() => resetErrorBoundary(error)}>
+          <i className="material-icons">arrow_back</i>
+          Reload page
+        </BackButton>
+        <Splitter>|</Splitter>
+        <Helper>
+          Sorry for the inconvinience! The Porter team has been notified
+        </Helper>
+      </Flex>
+    </StyledPageNotFound>
+  </>
+);
+
+export default UnexpectedErrorPage;
+
+const Splitter = styled.div`
+  margin: 0 20px;
+  font-size: 27px;
+  font-weight: 200;
+  color: #ffffff15;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const Helper = styled.div`
+  font-size: 15px;
+  max-width: 550px;
+  margin-right: -50px;
+`;
+
+const BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 16px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+    margin-left: -2px;
+  }
+`;
+
+const StyledPageNotFound = styled.div`
+  font-family: "Work Sans", sans-serif;
+  color: #6f6f6f;
+  font-size: 16px;
+  user-select: none;
+  margin-top: -80px;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+`;
+
+const Mega = styled.div`
+  font-size: 200px;
+  color: #ffffff06;
+  position: relative;
+  font-weight: bold;
+  text-align: center;
+
+  > i {
+    font-size: 23px;
+    margin-right: 12px;
+  }
+`;
+
+const Inside = styled.div`
+  position: absolute;
+  color: #6f6f6f;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: 400;
+  font-size: 20px;
+
+  > i {
+    font-size: 23px;
+    margin-right: 12px;
+  }
+`;

+ 1 - 1
dashboard/src/components/EventCard.tsx → dashboard/src/components/events/EventCard.tsx

@@ -1,6 +1,6 @@
 import React, { useState } from "react";
 import styled from "styled-components";
-import { Event } from "main/home/cluster-dashboard/expanded-chart/events/EventsTab";
+import { Event } from "./EventsContext";
 
 type CardProps = {
   event: Event;

+ 56 - 0
dashboard/src/components/events/EventLogs.tsx

@@ -0,0 +1,56 @@
+import React, { useContext } from "react";
+import styled from "styled-components";
+import backArrow from "assets/back_arrow.png";
+import { EventContext } from "./EventsContext";
+
+type EventLogsProps = {};
+
+const EventLogs: React.FunctionComponent<EventLogsProps> = ({}) => {
+  const { clearSelectedEvent } = useContext(EventContext);
+  return (
+    <>
+      <ControlRow>
+        <div>
+          <BackButton onClick={clearSelectedEvent}>
+            <BackButtonImg src={backArrow} />
+          </BackButton>
+        </div>
+      </ControlRow>
+      <div>Show logs</div>
+    </>
+  );
+};
+
+export default EventLogs;
+
+const ControlRow = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 35px;
+  padding-left: 0px;
+`;
+
+const BackButton = styled.div`
+  display: flex;
+  width: 36px;
+  cursor: pointer;
+  height: 36px;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+    > img {
+      opacity: 1;
+    }
+  }
+`;
+
+const BackButtonImg = styled.img`
+  width: 16px;
+  opacity: 0.75;
+`;

+ 215 - 0
dashboard/src/components/events/EventsContext.tsx

@@ -0,0 +1,215 @@
+import React, { createContext, useContext, useEffect, useState } from "react";
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+export type Event = {
+  id: number;
+  project_id: number;
+  cluster_id: number;
+  owner_name: string;
+  owner_type: string;
+  event_type: "critical" | "normal";
+  resource_type: string;
+  name: string;
+  namespace: string;
+  message: string;
+  reason: string;
+  timestamp: string;
+};
+
+type EventsContextType = {
+  isPorterAgentInstalled: boolean;
+  isPorterAgentInstalling: boolean;
+  isLoading: boolean;
+  eventList: Event[];
+  selectedEvent: Event | null;
+  selectEvent: (id: number) => void;
+  clearSelectedEvent: () => void;
+  setLimit: (limit: number) => void;
+  setResourceType: (newResourceType: "pod" | "hpa" | "node") => void;
+  installPorterAgent: () => Promise<void>;
+};
+
+const defaultEventContext: EventsContextType = {
+  eventList: [],
+  isPorterAgentInstalled: false,
+  isPorterAgentInstalling: false,
+  isLoading: true,
+  selectedEvent: null,
+  selectEvent: () => {},
+  clearSelectedEvent: () => {},
+  setLimit: () => {},
+  setResourceType: () => {},
+  installPorterAgent: async () => {},
+};
+
+export const EventContext = createContext<EventsContextType>(
+  defaultEventContext
+);
+
+type EventController = { type: string; name: string };
+
+type Props = {
+  controllers: EventController[];
+  enableNodeEvents: boolean;
+};
+
+const EventsContextProvider: React.FC<Props> = ({
+  children,
+  controllers,
+  enableNodeEvents,
+}) => {
+  // Porter agent related
+  const [isPorterAgentInstalled, setIsPorterAgentInstalled] = useState<boolean>(
+    false
+  );
+  const [
+    isPorterAgentInstalling,
+    setIsPorterAgentInstalling,
+  ] = useState<boolean>(false);
+  const [isLoading, setIsLoading] = useState<boolean>(false);
+
+  // Event related
+  const [eventList, setEventList] = useState<Event[]>([]);
+  const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
+  const [selectedController, setSelectedController] = useState<EventController>(
+    () => controllers[0] || undefined
+  );
+
+  // Pagination related
+  const [limit, setLimit] = useState<number>(10);
+  const [resourceType, setResourceType] = useState<"pod" | "hpa" | "node">(
+    "pod"
+  );
+  // Currently only implemented one sort type
+  const [sortBy] = useState<"timestamp">("timestamp");
+
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
+
+  useEffect(() => {
+    checkIfPorterAgentIsInstalled();
+  }, [currentCluster, currentProject]);
+
+  useEffect(() => {
+    if (!isPorterAgentInstalling) {
+      return () => {};
+    }
+    const interval = setInterval(() => {
+      checkIfPorterAgentIsInstalled();
+    }, 500);
+
+    return () => clearInterval(interval);
+  }, [isPorterAgentInstalling]);
+
+  useEffect(() => {
+    setIsLoading(true);
+    // Clear out event list if the resource type or the selected controller changed
+    if (
+      resourceType !== eventList[0]?.resource_type ||
+      selectedController.name !== eventList[0].name
+    ) {
+      setEventList([]);
+    }
+
+    getEventList();
+  }, [isPorterAgentInstalled, selectedController, resourceType, sortBy]);
+
+  const checkIfPorterAgentIsInstalled = async () => {
+    try {
+      await api.getPorterAgentIsInstalled(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+        },
+        {
+          project_id: currentProject.id,
+        }
+      );
+      setIsPorterAgentInstalled(true);
+    } catch (error) {
+      setIsPorterAgentInstalled(false);
+      setCurrentError(JSON.stringify(error));
+    }
+  };
+
+  const installPorterAgent = async () => {
+    try {
+      await api.installPorterAgent(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+        },
+        {
+          project_id: currentProject.id,
+        }
+      );
+      setIsPorterAgentInstalling(true);
+    } catch (error) {}
+  };
+
+  const removeDuplicatedEvents = (events: Event[]) => {
+    return events.reduce<Event[]>((prev, event, arr) => {
+      if (prev.find((e) => e.id === event.id)) {
+        return prev;
+      }
+      return [...prev, event];
+    }, []);
+  };
+
+  const getEventList = async () => {
+    try {
+      const res = await api.getEvents(
+        "<token>",
+        {
+          limit,
+          skip: eventList.length,
+          type: resourceType,
+          sort_by: sortBy,
+          owner_name: selectedController.name,
+          owner_type: selectedController.type,
+        },
+        {
+          cluster_id: currentCluster.id,
+          project_id: currentProject.id,
+        }
+      );
+      const newEventList = removeDuplicatedEvents([...eventList, ...res.data]);
+      setEventList(newEventList);
+    } catch (error) {
+      setEventList([]);
+      setCurrentError(JSON.stringify(error));
+    }
+  };
+
+  const selectEvent = (id: number) => {
+    const event = eventList.find((e) => e.id === id);
+    setSelectedEvent(event);
+  };
+
+  const clearSelectedEvent = () => {
+    setSelectedEvent(null);
+  };
+
+  return (
+    <EventContext.Provider
+      value={{
+        isPorterAgentInstalled,
+        isPorterAgentInstalling,
+        isLoading,
+        eventList,
+        selectedEvent,
+        selectEvent,
+        clearSelectedEvent,
+        setLimit,
+        setResourceType,
+        installPorterAgent,
+      }}
+    >
+      {children}
+    </EventContext.Provider>
+  );
+};
+
+export default EventsContextProvider;

+ 142 - 0
dashboard/src/components/events/EventsList.tsx

@@ -0,0 +1,142 @@
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+import Dropdown from "components/Dropdown";
+import { Event, EventContext } from "./EventsContext";
+import EventCard from "./EventCard";
+
+const mockData = [
+  {
+    id: 1,
+    project_id: 1,
+    cluster_id: 6,
+    owner_name: "pod-test",
+    owner_type: "deployment",
+    event_type: "critical",
+    resource_type: "pod",
+    name: "pod-test-1",
+    namespace: "default",
+    message: "",
+    reason: "OOM killed",
+    timestamp: "2021-06-30T21:48:23Z",
+  },
+  {
+    id: 2,
+    project_id: 1,
+    cluster_id: 6,
+    owner_name: "pod-test",
+    owner_type: "deployment",
+    event_type: "normal",
+    resource_type: "pod",
+    name: "pod-test-2",
+    namespace: "default",
+    message: "",
+    reason: "OOM killed",
+    timestamp: "2021-06-30T21:48:23Z",
+  },
+  {
+    id: 3,
+    project_id: 1,
+    cluster_id: 6,
+    owner_name: "pod-test",
+    owner_type: "deployment",
+    event_type: "critical",
+    resource_type: "pod",
+    name: "pod-test-2",
+    namespace: "default",
+    message: "",
+    reason: "OOM killed",
+    timestamp: "2021-06-30T21:48:23Z",
+  },
+  {
+    id: 4,
+    project_id: 1,
+    cluster_id: 6,
+    owner_name: "pod-test",
+    owner_type: "deployment",
+    event_type: "critical",
+    resource_type: "pod",
+    name: "pod-test-2",
+    namespace: "default",
+    message: "",
+    reason: "OOM killed",
+    timestamp: "2021-06-30T21:48:23Z",
+  },
+  {
+    id: 5,
+    project_id: 1,
+    cluster_id: 6,
+    owner_name: "pod-test",
+    owner_type: "deployment",
+    event_type: "critical",
+    resource_type: "pod",
+    name: "pod-test-2",
+    namespace: "default",
+    message: "",
+    reason: "OOM killed",
+    timestamp: "2021-06-30T21:48:23Z",
+  },
+  {
+    id: 6,
+    project_id: 1,
+    cluster_id: 6,
+    owner_name: "pod-test",
+    owner_type: "deployment",
+    event_type: "critical",
+    resource_type: "pod",
+    name: "pod-test-2",
+    namespace: "default",
+    message: "",
+    reason: "OOM killed",
+    timestamp: "2021-06-30T21:48:23Z",
+  },
+];
+
+const EventsList: React.FunctionComponent = ({}) => {
+  const { eventList, selectEvent, setResourceType } = useContext(EventContext);
+
+  const handleEventTypeSelection = (option: {
+    label: string;
+    value: "pod" | "hpa";
+  }) => {
+    setResourceType(option.value);
+  };
+
+  return (
+    <div>
+      <ControlRow>
+        <div>
+          <Dropdown
+            options={[
+              { label: "Pods", value: "pod" },
+              { label: "HPA", value: "hpa" },
+            ]}
+            onSelect={handleEventTypeSelection}
+          />
+        </div>
+      </ControlRow>
+      <EventsGrid>
+        {eventList.map((event) => {
+          return (
+            <EventCard key={event.id} event={event} selectEvent={selectEvent} />
+          );
+        })}
+      </EventsGrid>
+    </div>
+  );
+};
+
+export default EventsList;
+
+const ControlRow = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 35px;
+  padding-left: 0px;
+`;
+
+const EventsGrid = styled.div`
+  display: grid;
+  grid-row-gap: 15px;
+  grid-template-columns: 1;
+`;

+ 1 - 2
dashboard/src/index.html

@@ -59,14 +59,13 @@
               n.parentNode.insertBefore(t, n);
               analytics._loadOptions = e;
             };
-            analytics._writeKey = "ZKKaKBrAw9BGE8aF8XDoupd7Fi6ZyN5b";
+            analytics._writeKey = "J6sN7XaMPOGIkA1ZGYMBU4UX37aPZ1Yb";
             analytics.SNIPPET_VERSION = "4.13.2";
             analytics.load("<%= htmlWebpackPlugin.options.segmentKey %>");
             analytics.page();
           }
       })();
     </script>
-
     <link rel="icon" href="https://i.ibb.co/HnSk02f/ptr.png" />
     <meta
       name="description"

+ 3 - 36
dashboard/src/main/Main.tsx

@@ -1,6 +1,5 @@
 import React, { Component } from "react";
-import styled, { createGlobalStyle } from "styled-components";
-import { Redirect, Route, Switch } from "react-router-dom";
+import { Route, Redirect, Switch } from "react-router-dom";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
@@ -208,44 +207,12 @@ export default class Main extends Component<PropsType, StateType> {
 
   render() {
     return (
-      <StyledMain>
-        <GlobalStyle />
+      <>
         {this.renderMain()}
         <CurrentError currentError={this.context.currentError} />
-      </StyledMain>
+      </>
     );
   }
 }
 
 Main.contextType = Context;
-
-const GlobalStyle = createGlobalStyle`
-  * {
-    box-sizing: border-box;
-    font-family: 'Work Sans', sans-serif;
-  }
-  
-  body {
-    background: #202227;
-    overscroll-behavior-x: none;
-  }
-
-  a {
-    color: #949eff;
-    text-decoration: none;
-  }
-
-  img {
-    max-width: 100%;
-  }
-`;
-
-const StyledMain = styled.div`
-  height: 100vh;
-  width: 100vw;
-  position: fixed;
-  top: 0;
-  left: 0;
-  background: #202227;
-  color: white;
-`;

+ 25 - 217
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx

@@ -1,232 +1,40 @@
-import React, { useEffect, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
-import backArrow from "assets/back_arrow.png";
-import Dropdown from "components/Dropdown";
-import EventCard from "components/EventCard";
-import EventLogs from "components/EventLogs";
 
-const mockData = [
-  {
-    id: 1,
-    project_id: 1,
-    cluster_id: 6,
-    owner_name: "pod-test",
-    owner_type: "deployment",
-    event_type: "critical",
-    resource_type: "pod",
-    name: "pod-test-1",
-    namespace: "default",
-    message: "",
-    reason: "OOM killed",
-    timestamp: "2021-06-30T21:48:23Z",
-  },
-  {
-    id: 2,
-    project_id: 1,
-    cluster_id: 6,
-    owner_name: "pod-test",
-    owner_type: "deployment",
-    event_type: "normal",
-    resource_type: "pod",
-    name: "pod-test-2",
-    namespace: "default",
-    message: "",
-    reason: "OOM killed",
-    timestamp: "2021-06-30T21:48:23Z",
-  },
-  {
-    id: 3,
-    project_id: 1,
-    cluster_id: 6,
-    owner_name: "pod-test",
-    owner_type: "deployment",
-    event_type: "critical",
-    resource_type: "pod",
-    name: "pod-test-2",
-    namespace: "default",
-    message: "",
-    reason: "OOM killed",
-    timestamp: "2021-06-30T21:48:23Z",
-  },
-  {
-    id: 4,
-    project_id: 1,
-    cluster_id: 6,
-    owner_name: "pod-test",
-    owner_type: "deployment",
-    event_type: "critical",
-    resource_type: "pod",
-    name: "pod-test-2",
-    namespace: "default",
-    message: "",
-    reason: "OOM killed",
-    timestamp: "2021-06-30T21:48:23Z",
-  },
-  {
-    id: 5,
-    project_id: 1,
-    cluster_id: 6,
-    owner_name: "pod-test",
-    owner_type: "deployment",
-    event_type: "critical",
-    resource_type: "pod",
-    name: "pod-test-2",
-    namespace: "default",
-    message: "",
-    reason: "OOM killed",
-    timestamp: "2021-06-30T21:48:23Z",
-  },
-  {
-    id: 6,
-    project_id: 1,
-    cluster_id: 6,
-    owner_name: "pod-test",
-    owner_type: "deployment",
-    event_type: "critical",
-    resource_type: "pod",
-    name: "pod-test-2",
-    namespace: "default",
-    message: "",
-    reason: "OOM killed",
-    timestamp: "2021-06-30T21:48:23Z",
-  },
-];
-
-export type Event = {
-  id: number;
-  project_id: number;
-  cluster_id: number;
-  owner_name: string;
-  owner_type: string;
-  event_type: "critical" | "normal";
-  resource_type: string;
-  name: string;
-  namespace: string;
-  message: string;
-  reason: string;
-  timestamp: string;
-};
+import EventLogs from "components/events/EventLogs";
+import EventsList from "components/events/EventsList";
+import EventsContextProvider, {
+  EventContext,
+} from "components/events/EventsContext";
 
 type EventsTabProps = {};
 
 const EventsTab: React.FunctionComponent<EventsTabProps> = () => {
-  const [eventList, setEventList] = useState<Event[]>([]);
-  const [selectedEvent, setSelectedEvent] = useState<Event>(null);
-  const [currentFilter, setCurrentFilter] = useState<string>("all");
-
-  useEffect(() => {
-    setTimeout(() => {
-      setEventList(
-        (mockData as Event[]).filter(
-          (e) => currentFilter === "all" || e.resource_type === currentFilter
-        )
-      );
-    }, 500);
-  }, [currentFilter]);
-
-  const selectEvent = (id: number) => {
-    const event = eventList.find((e) => e.id === id);
-    setSelectedEvent(event);
-  };
-
-  const clearSelectedEvent = () => {
-    setSelectedEvent(null);
-  };
-
-  const handleEventTypeSelection = (option: {
-    label: string;
-    value: string;
-  }) => {
-    console.log(option);
-    setCurrentFilter(option.value);
-  };
-
   return (
-    <NamespaceListWrapper>
-      {!selectedEvent && (
-        <>
-          <ControlRow>
-            <div>
-              <Dropdown
-                options={[
-                  { label: "All", value: "all" },
-                  { label: "Pods", value: "pod" },
-                  { label: "HPA", value: "HPA" },
-                ]}
-                onSelect={handleEventTypeSelection}
-              />
-            </div>
-          </ControlRow>
-          <EventsGrid>
-            {eventList.map((event) => {
-              return (
-                <EventCard
-                  key={event.id}
-                  event={event}
-                  selectEvent={selectEvent}
-                />
-              );
-            })}
-          </EventsGrid>
-        </>
-      )}
-      {selectedEvent && (
-        <>
-          <ControlRow>
-            <div>
-              <BackButton onClick={clearSelectedEvent}>
-                <BackButtonImg src={backArrow} />
-              </BackButton>
-            </div>
-          </ControlRow>
-          <EventLogs event={selectedEvent} />
-        </>
-      )}
-    </NamespaceListWrapper>
+    <EventsContextProvider controllers={[]} enableNodeEvents={false}>
+      <EventContext.Consumer>
+        {({ selectedEvent }) => (
+          <EventsPageWrapper>
+            {!selectedEvent && (
+              <>
+                <EventsList />
+              </>
+            )}
+            {selectedEvent && (
+              <>
+                <EventLogs />
+              </>
+            )}
+          </EventsPageWrapper>
+        )}
+      </EventContext.Consumer>
+    </EventsContextProvider>
   );
 };
 
 export default EventsTab;
 
-const NamespaceListWrapper = styled.div`
+const EventsPageWrapper = styled.div`
   margin-top: 35px;
   padding-bottom: 80px;
 `;
-
-const ControlRow = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 35px;
-  padding-left: 0px;
-`;
-
-const EventsGrid = styled.div`
-  display: grid;
-  grid-row-gap: 15px;
-  grid-template-columns: 1;
-`;
-
-const BackButton = styled.div`
-  display: flex;
-  width: 36px;
-  cursor: pointer;
-  height: 36px;
-  align-items: center;
-  justify-content: center;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  background: #ffffff11;
-
-  :hover {
-    background: #ffffff22;
-    > img {
-      opacity: 1;
-    }
-  }
-`;
-
-const BackButtonImg = styled.img`
-  width: 16px;
-  opacity: 0.75;
-`;

+ 39 - 0
dashboard/src/shared/PorterErrorBoundary.tsx

@@ -0,0 +1,39 @@
+import UnexpectedErrorPage from "components/UnexpectedErrorPage";
+import React from "react";
+import { ErrorBoundary } from "react-error-boundary";
+
+export type PorterErrorBoundaryProps<OnResetProps = {}> = {
+  errorBoundaryLocation: string;
+  onReset?: (props: OnResetProps) => unknown;
+};
+
+const PorterErrorBoundary: React.FC<PorterErrorBoundaryProps> = ({
+  errorBoundaryLocation,
+  onReset,
+  children,
+}) => {
+  const handleError = (error: Error, info: { componentStack: string }) => {
+    window?.analytics?.track("React Error", {
+      location: errorBoundaryLocation,
+      error: error.message,
+      componentStack: info?.componentStack,
+      url: window.location.toString(),
+    });
+  };
+
+  const handleOnReset = (props: unknown) => {
+    typeof onReset === "function" ? onReset(props) : window.location.reload();
+  };
+
+  return (
+    <ErrorBoundary
+      onError={handleError}
+      FallbackComponent={UnexpectedErrorPage}
+      onReset={handleOnReset}
+    >
+      {children}
+    </ErrorBoundary>
+  );
+};
+
+export default PorterErrorBoundary;

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

@@ -1005,6 +1005,56 @@ const createWebhookToken = baseApi<
     `/api/projects/${project_id}/releases/${chart_name}/webhook_token?namespace=${namespace}&cluster_id=${cluster_id}&storage=${storage}`
 );
 
+const getPorterAgentIsInstalled = baseApi<
+  {
+    cluster_id: number;
+  },
+  { project_id: number }
+>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/k8s/agent/detect`;
+});
+
+const installPorterAgent = baseApi<
+  {
+    cluster_id: number;
+  },
+  { project_id: number }
+>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/k8s/agent/deploy`;
+});
+
+const getEvents = baseApi<
+  {
+    limit: number;
+    skip: number;
+    type: "pod" | "node" | "hpa";
+    owner_type: string;
+    owner_name: string;
+    sort_by: "timestamp";
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+  }
+>(
+  "GET",
+  (pathParams) =>
+    `api/projects/${pathParams.project_id}/clusters/${pathParams.cluster_id}/events`
+);
+
+const getEventById = baseApi<
+  {},
+  {
+    project_id: number;
+    cluster_id: number;
+    event_id: number;
+  }
+>(
+  "GET",
+  ({ project_id, cluster_id, event_id }) =>
+    `/api/projects/${project_id}/clusters/${cluster_id}/events/${event_id}`
+);
+
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
   checkAuth,
@@ -1108,4 +1158,8 @@ export default {
   removeCollaborator,
   getPolicyDocument,
   createWebhookToken,
+  getPorterAgentIsInstalled,
+  installPorterAgent,
+  getEvents,
+  getEventById,
 };

+ 10 - 0
server/api/release_handler.go

@@ -777,6 +777,8 @@ func (app *App) HandleGetReleaseToken(w http.ResponseWriter, r *http.Request) {
 			Code:   ErrReleaseReadData,
 			Errors: []string{"release not found"},
 		}, w)
+
+		return
 	}
 
 	release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, namespace)
@@ -786,6 +788,8 @@ func (app *App) HandleGetReleaseToken(w http.ResponseWriter, r *http.Request) {
 			Code:   ErrReleaseReadData,
 			Errors: []string{"release not found"},
 		}, w)
+
+		return
 	}
 
 	releaseExt := release.Externalize()
@@ -807,6 +811,8 @@ func (app *App) HandleCreateWebhookToken(w http.ResponseWriter, r *http.Request)
 			Code:   ErrReleaseReadData,
 			Errors: []string{"release not found"},
 		}, w)
+
+		return
 	}
 
 	// read the release from the target cluster
@@ -1056,6 +1062,8 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 				Code:   ErrReleaseReadData,
 				Errors: []string{"release not found"},
 			}, w)
+
+			return
 		}
 
 		release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, rel.Namespace)
@@ -1458,6 +1466,8 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 				Code:   ErrReleaseReadData,
 				Errors: []string{"release not found"},
 			}, w)
+
+			return
 		}
 
 		release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, rel.Namespace)

+ 15 - 5
server/api/user_handler.go

@@ -838,20 +838,30 @@ func (app *App) sendUser(w http.ResponseWriter, userID uint, email string, email
 }
 
 func (app *App) getUserIDFromRequest(r *http.Request) (uint, error) {
+	// first, check for token
+	tok := app.getTokenFromRequest(r)
+
+	if tok != nil {
+		return tok.IBy, nil
+	}
+
 	session, err := app.Store.Get(r, app.ServerConf.CookieName)
 
 	if err != nil {
 		return 0, err
 	}
 
-	// first, check for token
-	tok := app.getTokenFromRequest(r)
+	sessID, ok := session.Values["user_id"]
 
-	if tok != nil {
-		return tok.IBy, nil
+	if !ok {
+		return 0, fmt.Errorf("could not get user id from session")
 	}
 
-	userID, _ := session.Values["user_id"].(uint)
+	userID, ok := sessID.(uint)
+
+	if !ok {
+		return 0, fmt.Errorf("could not get user id from session")
+	}
 
 	return userID, nil
 }

+ 21 - 3
server/middleware/auth.go

@@ -210,7 +210,13 @@ func (auth *Auth) DoesUserHaveProjectAccess(
 			}
 
 			sessionUserID, ok := session.Values["user_id"]
-			userID = sessionUserID.(uint)
+
+			if !ok {
+				http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+				return
+			}
+
+			userID, ok = sessionUserID.(uint)
 
 			if !ok {
 				http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
@@ -440,7 +446,13 @@ func (auth *Auth) DoesUserHaveGitInstallationAccess(
 			}
 
 			sessionUserID, ok := session.Values["user_id"]
-			userID = sessionUserID.(uint)
+
+			if !ok {
+				http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+				return
+			}
+
+			userID, ok = sessionUserID.(uint)
 
 			if !ok {
 				http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
@@ -741,7 +753,13 @@ func (auth *Auth) DoesUserHaveDOIntegrationAccess(
 func (auth *Auth) doesSessionMatchID(r *http.Request, id uint) bool {
 	session, _ := auth.store.Get(r, auth.cookieName)
 
-	if sessID, ok := session.Values["user_id"].(uint); !ok || sessID != id {
+	userID, ok := session.Values["user_id"]
+
+	if !ok {
+		return false
+	}
+
+	if sessID, ok := userID.(uint); !ok || sessID != id {
 		return false
 	}
 

+ 11 - 0
services/porter_cli_container/Dockerfile

@@ -0,0 +1,11 @@
+FROM ubuntu:latest
+
+COPY get-porter-cli.sh /scratch/
+
+RUN apt-get update && apt-get install -y curl unzip
+
+ARG VERSION
+
+RUN /scratch/get-porter-cli.sh
+
+ENTRYPOINT ["porter"]

+ 15 - 0
services/porter_cli_container/get-porter-cli.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+if [[ -z $VERSION ]]; then
+  name=$(curl -s https://api.github.com/repos/porter-dev/porter/releases/latest | grep "browser_download_url.*/porter_.*_Linux_x86_64\.zip" | cut -d ":" -f 2,3 | tr -d \")
+  name=$(basename "$name")
+  curl -L https://github.com/porter-dev/porter/releases/latest/download/"$name" --output "$name"
+else
+  name=porter-$VERSION.zip
+  curl -L https://github.com/porter-dev/porter/releases/download/"$VERSION"/porter_"$VERSION"_Linux_x86_64.zip --output "$name"
+fi
+
+unzip -a "$name"
+rm "$name"
+chmod +x ./porter
+mv ./porter /usr/local/bin/