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

Merge branch 'master' into 0.7.0-better-metrics

Ivan Galakhov 4 лет назад
Родитель
Сommit
46dabda7a3
100 измененных файлов с 829 добавлено и 662 удалено
  1. 39 12
      .github/workflows/release.yaml
  2. 1 1
      cli/cmd/api/git_repo.go
  3. 8 9
      cli/cmd/api/github_action.go
  4. 0 20
      cli/cmd/connect.go
  5. 0 125
      cli/cmd/connect/actions.go
  6. 9 0
      cli/cmd/create.go
  7. 10 3
      cli/cmd/deploy/create.go
  8. 4 2
      cli/cmd/docker/agent.go
  9. 6 2
      cli/cmd/errors.go
  10. 9 2
      cli/cmd/logs.go
  11. 117 65
      cli/cmd/run.go
  12. 1 1
      cli/cmd/utils/browser.go
  13. 1 1
      cli/cmd/version.go
  14. 8 0
      dashboard/package-lock.json
  15. 2 1
      dashboard/package.json
  16. 41 3
      dashboard/src/App.tsx
  17. 0 1
      dashboard/src/components/CopyToClipboard.tsx
  18. 0 1
      dashboard/src/components/Table.tsx
  19. 117 0
      dashboard/src/components/UnexpectedErrorPage.tsx
  20. 0 1
      dashboard/src/components/form-components/KeyValueArray.tsx
  21. 6 6
      dashboard/src/components/porter-form/PorterForm.tsx
  22. 27 7
      dashboard/src/components/porter-form/PorterFormContextProvider.tsx
  23. 3 0
      dashboard/src/components/porter-form/PorterFormWrapper.tsx
  24. 1 5
      dashboard/src/components/porter-form/field-components/ArrayInput.tsx
  25. 1 6
      dashboard/src/components/porter-form/field-components/Checkbox.tsx
  26. 0 3
      dashboard/src/components/porter-form/field-components/Input.tsx
  27. 1 6
      dashboard/src/components/porter-form/field-components/KeyValueArray.tsx
  28. 1 6
      dashboard/src/components/porter-form/field-components/Select.tsx
  29. 1 1
      dashboard/src/components/porter-form/field-components/ServiceRow.tsx
  30. 1 5
      dashboard/src/components/porter-form/hooks/useFormField.tsx
  31. 1 1
      dashboard/src/components/repo-selector/ActionConfEditor.tsx
  32. 0 2
      dashboard/src/components/repo-selector/ActionDetails.tsx
  33. 1 1
      dashboard/src/components/repo-selector/BranchList.tsx
  34. 1 1
      dashboard/src/components/repo-selector/ContentsList.tsx
  35. 2 3
      dashboard/src/components/repo-selector/RepoList.tsx
  36. 1 2
      dashboard/src/index.html
  37. 3 36
      dashboard/src/main/Main.tsx
  38. 0 1
      dashboard/src/main/MainWrapper.tsx
  39. 1 1
      dashboard/src/main/auth/Register.tsx
  40. 1 2
      dashboard/src/main/auth/VerifyEmail.tsx
  41. 1 1
      dashboard/src/main/home/Home.tsx
  42. 2 7
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  43. 1 1
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  44. 1 1
      dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx
  45. 0 2
      dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx
  46. 0 1
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArray.tsx
  47. 1 4
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  48. 2 16
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  49. 2 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx
  50. 11 32
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  51. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx
  52. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx
  53. 2 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  54. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx
  55. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx
  56. 0 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx
  57. 1 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx
  58. 55 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/TempJobList.tsx
  59. 7 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx
  60. 6 3
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts
  61. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  62. 1 5
      dashboard/src/main/home/dashboard/ClusterList.tsx
  63. 1 1
      dashboard/src/main/home/dashboard/Dashboard.tsx
  64. 1 3
      dashboard/src/main/home/integrations/IntegrationCategories.tsx
  65. 1 1
      dashboard/src/main/home/integrations/Integrations.tsx
  66. 1 1
      dashboard/src/main/home/integrations/SlackIntegrationList.tsx
  67. 3 11
      dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx
  68. 1 6
      dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx
  69. 1 1
      dashboard/src/main/home/modals/DeleteNamespaceModal.tsx
  70. 1 1
      dashboard/src/main/home/modals/EnvEditorModal.tsx
  71. 1 4
      dashboard/src/main/home/modals/LoadEnvGroupModal.tsx
  72. 1 1
      dashboard/src/main/home/modals/UpgradeChartModal.tsx
  73. 1 4
      dashboard/src/main/home/navbar/Navbar.tsx
  74. 1 7
      dashboard/src/main/home/project-settings/InviteList.tsx
  75. 2 2
      dashboard/src/main/home/provisioner/AWSFormSection.tsx
  76. 1 2
      dashboard/src/main/home/provisioner/DOFormSection.tsx
  77. 1 1
      dashboard/src/main/home/provisioner/ExistingClusterSection.tsx
  78. 2 2
      dashboard/src/main/home/provisioner/GCPFormSection.tsx
  79. 0 2
      dashboard/src/main/home/provisioner/Provisioner.tsx
  80. 0 1
      dashboard/src/main/home/provisioner/ProvisionerLogs.tsx
  81. 1 1
      dashboard/src/main/home/sidebar/ProjectSection.tsx
  82. 0 1
      dashboard/src/main/home/sidebar/ProjectSectionContainer.tsx
  83. 1 2
      dashboard/src/main/home/sidebar/Sidebar.tsx
  84. 1 6
      dashboard/src/shared/Context.tsx
  85. 39 0
      dashboard/src/shared/PorterErrorBoundary.tsx
  86. 0 1
      dashboard/src/shared/api.tsx
  87. 1 2
      dashboard/src/shared/auth/AuthorizationHoc.tsx
  88. 2 2
      dashboard/src/shared/auth/RouteGuard.tsx
  89. 0 1
      dashboard/src/shared/common.tsx
  90. 0 2
      dashboard/src/shared/routing.tsx
  91. 21 9
      docs/guides/running-porter-locally.md
  92. 25 0
      docs/guides/slack-integration.md
  93. 10 10
      internal/forms/git_action.go
  94. 12 27
      internal/integrations/ci/actions/actions.go
  95. 12 29
      internal/integrations/ci/actions/steps.go
  96. 131 57
      internal/integrations/slack/notifier.go
  97. 1 1
      internal/kubernetes/config.go
  98. 2 0
      internal/models/gitrepo.go
  99. 9 5
      internal/models/integrations/gcp.go
  100. 16 7
      internal/registry/registry.go

+ 39 - 12
.github/workflows/release.yaml

@@ -27,11 +27,8 @@ jobs:
         run: |
           cat >./dashboard/.env <<EOL
           NODE_ENV=production
-          API_SERVER=dashboard.getporter.dev
-          FULLSTORY_ORG_ID=${{secrets.FULLSTORY_ORG_ID}}
-          DISCORD_KEY=${{secrets.DISCORD_KEY}}
-          DISCORD_CID=${{secrets.DISCORD_CID}}
-          FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
+          APPLICATION_CHART_REPO_URL=https://charts.getporter.dev
+          ADDON_CHART_REPO_URL=https://chart-addons.getporter.dev
           EOL
 
           cat ./dashboard/.env
@@ -62,13 +59,8 @@ jobs:
         run: |
           cat >./dashboard/.env <<EOL
           NODE_ENV=production
-          API_SERVER=dashboard.getporter.dev
-          FULLSTORY_ORG_ID=${{secrets.FULLSTORY_ORG_ID}}
-          DISCORD_KEY=${{secrets.DISCORD_KEY}}
-          DISCORD_CID=${{secrets.DISCORD_CID}}
-          FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
-          POSTHOG_API_KEY=${{secrets.POSTHOG_API_KEY}}
-          POSTHOG_HOST=${{secrets.POSTHOG_HOST}}
+          APPLICATION_CHART_REPO_URL=https://charts.getporter.dev
+          ADDON_CHART_REPO_URL=https://chart-addons.getporter.dev
           EOL
       - name: Build and zip static folder
         run: |
@@ -355,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(

+ 8 - 9
cli/cmd/api/github_action.go

@@ -11,15 +11,14 @@ import (
 // CreateGithubActionRequest represents the accepted fields for creating
 // a Github action
 type CreateGithubActionRequest struct {
-	ReleaseID      uint              `json:"release_id" form:"required"`
-	GitRepo        string            `json:"git_repo" form:"required"`
-	GitBranch      string            `json:"git_branch"`
-	ImageRepoURI   string            `json:"image_repo_uri" form:"required"`
-	DockerfilePath string            `json:"dockerfile_path"`
-	FolderPath     string            `json:"folder_path"`
-	GitRepoID      uint              `json:"git_repo_id" form:"required"`
-	BuildEnv       map[string]string `json:"env"`
-	RegistryID     uint              `json:"registry_id"`
+	ReleaseID      uint   `json:"release_id" form:"required"`
+	GitRepo        string `json:"git_repo" form:"required"`
+	GitBranch      string `json:"git_branch"`
+	ImageRepoURI   string `json:"image_repo_uri" form:"required"`
+	DockerfilePath string `json:"dockerfile_path"`
+	FolderPath     string `json:"folder_path"`
+	GitRepoID      uint   `json:"git_repo_id" form:"required"`
+	RegistryID     uint   `json:"registry_id"`
 }
 
 // CreateGithubAction creates a Github action with basic authentication

+ 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
-}

+ 9 - 0
cli/cmd/create.go

@@ -74,6 +74,7 @@ var name string
 var values string
 var source string
 var image string
+var registryURL string
 
 func init() {
 	rootCmd.AddCommand(createCmd)
@@ -137,6 +138,13 @@ func init() {
 		"",
 		"if the source is \"registry\", the image to use, in repository:tag format",
 	)
+
+	createCmd.PersistentFlags().StringVar(
+		&registryURL,
+		"registry-url",
+		"",
+		"the registry URL to use (must exist in \"porter registries list\")",
+	)
 }
 
 var supportedKinds = map[string]string{"web": "", "job": "", "worker": ""}
@@ -183,6 +191,7 @@ func createFull(resp *api.AuthCheckResponse, client *api.Client, args []string)
 			},
 			Kind:        args[0],
 			ReleaseName: name,
+			RegistryURL: registryURL,
 		},
 	}
 

+ 10 - 3
cli/cmd/deploy/create.go

@@ -25,6 +25,7 @@ type CreateOpts struct {
 
 	Kind        string
 	ReleaseName string
+	RegistryURL string
 }
 
 // GithubOpts are the options for linking a Github source to the app
@@ -59,7 +60,7 @@ func (c *CreateAgent) CreateFromGithub(
 		githubRepos, err := c.Client.ListGithubRepos(
 			context.Background(),
 			c.CreateOpts.ProjectID,
-			gitRepo.ID,
+			gitRepo,
 		)
 
 		if err != nil {
@@ -68,7 +69,7 @@ func (c *CreateAgent) CreateFromGithub(
 
 		for _, githubRepo := range githubRepos {
 			if githubRepo.FullName == ghOpts.Repo {
-				gitRepoMatch = gitRepo.ID
+				gitRepoMatch = gitRepo
 				break
 			}
 		}
@@ -367,7 +368,13 @@ func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, err
 	var regID uint
 
 	for _, reg := range registries {
-		if reg.URL != "" {
+		if c.CreateOpts.RegistryURL != "" {
+			if c.CreateOpts.RegistryURL == reg.URL {
+				regID = reg.ID
+				imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
+				break
+			}
+		} else if reg.URL != "" {
 			regID = reg.ID
 			imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
 			break

+ 4 - 2
cli/cmd/docker/agent.go

@@ -200,12 +200,14 @@ func (a *Agent) PushImage(image string) error {
 		opts,
 	)
 
+	if out != nil {
+		defer out.Close()
+	}
+
 	if err != nil {
 		return err
 	}
 
-	defer out.Close()
-
 	termFd, isTerm := term.GetFdInfo(os.Stderr)
 
 	return jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil)

+ 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())

+ 9 - 2
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)
@@ -96,11 +97,17 @@ func logs(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 		selectedContainerName = selectedContainer
 	}
 
-	restConf, err := getRESTConfig(client)
+	config := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err = config.setSharedConfig()
 
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 
-	return pipePodLogsToStdout(restConf, namespace, selectedPod.Name, selectedContainerName, follow)
+	_, err = pipePodLogsToStdout(config, namespace, selectedPod.Name, selectedContainerName, follow)
+
+	return err
 }

+ 117 - 65
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 {
@@ -76,7 +85,7 @@ func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 
 	if len(podsSimple) == 0 {
 		return fmt.Errorf("At least one pod must exist in this deployment.")
-	} else if len(podsSimple) == 1 {
+	} else if len(podsSimple) == 1 || !existingPod {
 		selectedPod = podsSimple[0]
 	} else {
 		podNames := make([]string, 0)
@@ -116,27 +125,38 @@ func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 		selectedContainerName = selectedContainer
 	}
 
-	restConf, err := getRESTConfig(client)
+	config := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err = config.setSharedConfig()
 
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 
 	if existingPod {
-		return executeRun(restConf, namespace, selectedPod.Name, selectedContainerName, args[1:])
+		return executeRun(config, namespace, selectedPod.Name, selectedContainerName, args[1:])
 	}
 
-	return executeRunEphemeral(restConf, namespace, selectedPod.Name, selectedContainerName, args[1:])
+	return executeRunEphemeral(config, namespace, selectedPod.Name, selectedContainerName, args[1:])
+}
+
+type PorterRunSharedConfig struct {
+	Client     *api.Client
+	RestConf   *rest.Config
+	Clientset  *kubernetes.Clientset
+	RestClient *rest.RESTClient
 }
 
-func getRESTConfig(client *api.Client) (*rest.Config, error) {
+func (p *PorterRunSharedConfig) setSharedConfig() error {
 	pID := config.Project
 	cID := config.Cluster
 
-	kubeResp, err := client.GetKubeconfig(context.TODO(), pID, cID)
+	kubeResp, err := p.Client.GetKubeconfig(context.TODO(), pID, cID)
 
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	kubeBytes := kubeResp.Kubeconfig
@@ -144,13 +164,13 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 	cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
 
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	restConf, err := cmdConf.ClientConfig()
 
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	restConf.GroupVersion = &schema.GroupVersion{
@@ -160,7 +180,25 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 
 	restConf.NegotiatedSerializer = runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{})
 
-	return restConf, nil
+	p.RestConf = restConf
+
+	clientset, err := kubernetes.NewForConfig(restConf)
+
+	if err != nil {
+		return err
+	}
+
+	p.Clientset = clientset
+
+	restClient, err := rest.RESTClientFor(restConf)
+
+	if err != nil {
+		return err
+	}
+
+	p.RestClient = restClient
+
+	return nil
 }
 
 type podSimple struct {
@@ -196,14 +234,8 @@ func getPods(client *api.Client, namespace, releaseName string) ([]podSimple, er
 	return res, nil
 }
 
-func executeRun(config *rest.Config, namespace, name, container string, args []string) error {
-	restClient, err := rest.RESTClientFor(config)
-
-	if err != nil {
-		return err
-	}
-
-	req := restClient.Post().
+func executeRun(config *PorterRunSharedConfig, namespace, name, container string, args []string) error {
+	req := config.RestClient.Post().
 		Resource("pods").
 		Name(name).
 		Namespace(namespace).
@@ -224,7 +256,7 @@ func executeRun(config *rest.Config, namespace, name, container string, args []s
 	}
 
 	fn := func() error {
-		exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+		exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
 
 		if err != nil {
 			return err
@@ -242,10 +274,10 @@ func executeRun(config *rest.Config, namespace, name, container string, args []s
 		return err
 	}
 
-	return err
+	return nil
 }
 
-func executeRunEphemeral(config *rest.Config, namespace, name, container string, args []string) error {
+func executeRunEphemeral(config *PorterRunSharedConfig, namespace, name, container string, args []string) error {
 	existing, err := getExistingPod(config, name, namespace)
 
 	if err != nil {
@@ -267,13 +299,7 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 	}
 
 	fn := func() error {
-		restClient, err := rest.RESTClientFor(config)
-
-		if err != nil {
-			return err
-		}
-
-		req := restClient.Post().
+		req := config.RestClient.Post().
 			Resource("pods").
 			Name(podName).
 			Namespace("default").
@@ -284,7 +310,7 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 		req.Param("tty", "true")
 		req.Param("container", container)
 
-		exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+		exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
 
 		if err != nil {
 			return err
@@ -309,12 +335,25 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 
 		time.Sleep(2 * time.Second)
 
-		// ugly way to catch non-TTY errors, such as when running command "echo \"hello\""
-		if i == 4 && err != nil && strings.Contains(err.Error(), "not found in pod") {
-			fmt.Printf("Could not open a shell to this container. Container logs:\n")
+	}
+
+	// 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")
+
+		var writtenBytes int64
+
+		writtenBytes, err = pipePodLogsToStdout(config, namespace, podName, container, false)
 
-			err = pipePodLogsToStdout(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
@@ -323,75 +362,79 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 	return err
 }
 
-func pipePodLogsToStdout(config *rest.Config, namespace, name, container string, follow bool) error {
+func pipePodLogsToStdout(config *PorterRunSharedConfig, namespace, name, container string, follow bool) (int64, error) {
 	podLogOpts := v1.PodLogOptions{
 		Container: container,
 		Follow:    follow,
 	}
 
-	// creates the clientset
-	clientset, err := kubernetes.NewForConfig(config)
-
-	if err != nil {
-		return err
-	}
-
-	req := clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
+	req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
 
 	podLogs, err := req.Stream(
 		context.Background(),
 	)
 
 	if err != nil {
-		return err
+		return 0, err
 	}
 
 	defer podLogs.Close()
 
-	_, err = io.Copy(os.Stdout, podLogs)
+	return io.Copy(os.Stdout, podLogs)
+}
+
+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(),
+		metav1.ListOptions{
+			FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
+		},
+	)
 
 	if err != nil {
 		return err
 	}
 
+	for _, event := range resp.Items {
+		color.New(color.FgRed).Println(event.Message)
+	}
+
 	return nil
 }
 
-func getExistingPod(config *rest.Config, name, namespace string) (*v1.Pod, error) {
-	clientset, err := kubernetes.NewForConfig(config)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return clientset.CoreV1().Pods(namespace).Get(
+func getExistingPod(config *PorterRunSharedConfig, name, namespace string) (*v1.Pod, error) {
+	return config.Clientset.CoreV1().Pods(namespace).Get(
 		context.Background(),
 		name,
 		metav1.GetOptions{},
 	)
 }
 
-func deletePod(config *rest.Config, name, namespace string) error {
-	clientset, err := kubernetes.NewForConfig(config)
-
-	if err != nil {
-		return err
-	}
+func deletePod(config *PorterRunSharedConfig, name, namespace string) error {
+	// update the config in case the operation has taken longer than token expiry time
+	config.setSharedConfig()
 
-	return clientset.CoreV1().Pods(namespace).Delete(
+	err := config.Clientset.CoreV1().Pods(namespace).Delete(
 		context.Background(),
 		name,
 		metav1.DeleteOptions{},
 	)
-}
-
-func createPodFromExisting(config *rest.Config, existing *v1.Pod, args []string) (*v1.Pod, error) {
-	clientset, err := kubernetes.NewForConfig(config)
 
 	if err != nil {
-		return nil, err
+		color.New(color.FgRed).Println("Could not delete ephemeral pod: %s", err.Error())
+		return err
 	}
 
+	color.New(color.FgGreen).Println("Sucessfully deleted ephemeral pod")
+
+	return nil
+}
+
+func createPodFromExisting(config *PorterRunSharedConfig, existing *v1.Pod, args []string) (*v1.Pod, error) {
 	newPod := existing.DeepCopy()
 
 	// only copy the pod spec, overwrite metadata
@@ -402,6 +445,9 @@ func createPodFromExisting(config *rest.Config, existing *v1.Pod, args []string)
 
 	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
 
@@ -418,9 +464,15 @@ func createPodFromExisting(config *rest.Config, existing *v1.Pod, args []string)
 	newPod.Spec.Containers[0].TTY = true
 	newPod.Spec.Containers[0].Stdin = true
 	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 clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
+	return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
 		context.Background(),
 		newPod,
 		metav1.CreateOptions{},

+ 1 - 1
cli/cmd/utils/browser.go

@@ -11,7 +11,7 @@ func OpenBrowser(url string) error {
 	var cmd string
 	var args []string
 
-	fmt.Printf("Attempting to open your browser. If this does not work, please navigate to: %s", url)
+	fmt.Printf("Attempting to open your browser. If this does not work, please navigate to: %s\n", url)
 
 	switch runtime.GOOS {
 	case "windows":

+ 1 - 1
cli/cmd/version.go

@@ -7,7 +7,7 @@ import (
 )
 
 // Version will be linked by an ldflag during build
-var Version string = "v0.5.0"
+var Version string = "v0.8.0"
 
 var versionCmd = &cobra.Command{
 	Use:     "version",

+ 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 - 1
dashboard/src/components/CopyToClipboard.tsx

@@ -3,7 +3,6 @@ import ClipboardJS from "clipboard";
 import React, { Component, RefObject } from "react";
 import Tooltip from "@material-ui/core/Tooltip";
 import styled from "styled-components";
-import { styled as materialStyled } from "@material-ui/core/styles";
 
 type PropsType = {
   text: string;

+ 0 - 1
dashboard/src/components/Table.tsx

@@ -1,7 +1,6 @@
 import React from "react";
 import styled from "styled-components";
 import { Column, Row, useGlobalFilter, useTable } from "react-table";
-import InputRow from "./form-components/InputRow";
 import Loading from "components/Loading";
 
 const GlobalFilter: React.FunctionComponent<any> = ({ setGlobalFilter }) => {

+ 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;
+  }
+`;

+ 0 - 1
dashboard/src/components/form-components/KeyValueArray.tsx

@@ -6,7 +6,6 @@ import EnvEditorModal from "../../main/home/modals/EnvEditorModal";
 
 import sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
-import { keysIn } from "lodash";
 
 export type KeyValue = {
   key: string;

+ 6 - 6
dashboard/src/components/porter-form/PorterForm.tsx

@@ -1,14 +1,14 @@
-import React, { useContext, useState } from "react";
+import React, { useContext } from "react";
 import {
-  Section,
+  ArrayInputField,
+  CheckboxField,
   FormField,
   InputField,
-  CheckboxField,
   KeyValueArrayField,
-  ArrayInputField,
-  SelectField,
-  ServiceIPListField,
   ResourceListField,
+  Section,
+  SelectField,
+  ServiceIPListField
 } from "./types";
 import TabRegion, { TabOption } from "../TabRegion";
 import Heading from "../form-components/Heading";

+ 27 - 7
dashboard/src/components/porter-form/PorterFormContextProvider.tsx

@@ -1,11 +1,11 @@
 import React, { createContext, useContext, useReducer } from "react";
 import {
+  GetFinalVariablesFunction,
+  PorterFormAction,
   PorterFormData,
   PorterFormState,
-  PorterFormAction,
-  PorterFormVariableList,
   PorterFormValidationInfo,
-  GetFinalVariablesFunction,
+  PorterFormVariableList,
 } from "./types";
 import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
@@ -20,6 +20,7 @@ interface Props {
   onSubmit: (vars: PorterFormVariableList) => void;
   initialVariables?: PorterFormVariableList;
   overrideVariables?: PorterFormVariableList;
+  includeHiddenFields?: boolean;
   isReadOnly?: boolean;
   doDebug?: boolean;
 }
@@ -30,6 +31,7 @@ interface ContextProps {
   onSubmit: () => void;
   dispatchAction: (event: PorterFormAction) => void;
   validationInfo: PorterFormValidationInfo;
+  getSubmitValues: () => PorterFormVariableList;
   isReadOnly?: boolean;
 }
 
@@ -131,7 +133,17 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
         })
       )
     );
-    return ret;
+    return {
+      ...ret,
+      ...{
+        "currentCluster.service.is_gcp":
+          context.currentCluster?.service == "gke",
+        "currentCluster.service.is_aws":
+          context.currentCluster?.service == "eks",
+        "currentCluster.service.is_do":
+          context.currentCluster?.service == "doks",
+      },
+    };
   };
 
   const getInitialValidation = (data: PorterFormData) => {
@@ -376,7 +388,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
   using functions for each input to finalize the variables
   This can take care of things like appending units to strings
  */
-  const onSubmitWrapper = () => {
+  const getSubmitValues = () => {
     // we start off with a base list of the current variables for fields
     // that don't need any processing on top (for example: checkbox)
     // the assign here is important because that way state.variable isn't mutated
@@ -391,7 +403,9 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
       select: getFinalVariablesForSelect,
     };
 
-    const data = props.rawFormData.includeHiddenFields
+    const data = props.includeHiddenFields
+      ? restructureToNewFields(props.rawFormData)
+      : props.rawFormData.includeHiddenFields
       ? restructureToNewFields(props.rawFormData)
       : formData;
 
@@ -411,7 +425,12 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
       )
     );
     if (props.doDebug) console.log(Object.assign.apply({}, varList));
-    props.onSubmit(Object.assign.apply({}, varList));
+
+    return Object.assign.apply({}, varList);
+  };
+
+  const onSubmitWrapper = () => {
+    props.onSubmit(getSubmitValues());
   };
 
   if (props.doDebug) {
@@ -434,6 +453,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
           error: isValidated ? null : "Missing required fields",
         },
         onSubmit: onSubmitWrapper,
+        getSubmitValues,
       }}
     >
       {props.children}

+ 3 - 0
dashboard/src/components/porter-form/PorterFormWrapper.tsx

@@ -19,6 +19,7 @@ type PropsType = {
   saveValuesStatus?: string;
   showStateDebugger?: boolean;
   isLaunch?: boolean;
+  includeHiddenFields?: boolean;
 };
 
 const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
@@ -36,6 +37,7 @@ const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
   saveValuesStatus,
   showStateDebugger,
   isLaunch,
+  includeHiddenFields,
 }) => {
   const hashCode = (s: string) => {
     return s?.split("").reduce(function (a, b) {
@@ -72,6 +74,7 @@ const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
         overrideVariables={valuesToOverride}
         isReadOnly={isReadOnly}
         onSubmit={onSubmit}
+        includeHiddenFields={includeHiddenFields}
       >
         <PorterForm
           showStateDebugger={showStateDebugger}

+ 1 - 5
dashboard/src/components/porter-form/field-components/ArrayInput.tsx

@@ -1,10 +1,6 @@
 import React from "react";
 import styled from "styled-components";
-import {
-  ArrayInputField,
-  ArrayInputFieldState,
-  GetFinalVariablesFunction,
-} from "../types";
+import { ArrayInputField, ArrayInputFieldState, GetFinalVariablesFunction } from "../types";
 import useFormField from "../hooks/useFormField";
 
 const ArrayInput: React.FC<ArrayInputField> = (props) => {

+ 1 - 6
dashboard/src/components/porter-form/field-components/Checkbox.tsx

@@ -1,10 +1,5 @@
 import React from "react";
-import {
-  ArrayInputField,
-  CheckboxField,
-  CheckboxFieldState,
-  GetFinalVariablesFunction,
-} from "../types";
+import { CheckboxField, CheckboxFieldState, GetFinalVariablesFunction } from "../types";
 import CheckboxRow from "../../form-components/CheckboxRow";
 import useFormField from "../hooks/useFormField";
 

+ 0 - 3
dashboard/src/components/porter-form/field-components/Input.tsx

@@ -2,7 +2,6 @@ import React from "react";
 import InputRow from "../../form-components/InputRow";
 import useFormField from "../hooks/useFormField";
 import {
-  GenericInputField,
   GetFinalVariablesFunction,
   InputField,
   StringInputFieldState,
@@ -50,8 +49,6 @@ const Input: React.FC<InputField> = ({
     return <></>;
   }
 
-  console.log(value);
-
   const curValue =
     settings?.type == "number"
       ? !isNaN(parseFloat(variables[variable]))

+ 1 - 6
dashboard/src/components/porter-form/field-components/KeyValueArray.tsx

@@ -1,10 +1,5 @@
 import React from "react";
-import {
-  GetFinalVariablesFunction,
-  InputField,
-  KeyValueArrayField,
-  KeyValueArrayFieldState,
-} from "../types";
+import { GetFinalVariablesFunction, KeyValueArrayField, KeyValueArrayFieldState } from "../types";
 import sliders from "../../../assets/sliders.svg";
 import upload from "../../../assets/upload.svg";
 import styled from "styled-components";

+ 1 - 6
dashboard/src/components/porter-form/field-components/Select.tsx

@@ -1,10 +1,5 @@
 import React, { useContext } from "react";
-import {
-  CheckboxField,
-  GetFinalVariablesFunction,
-  SelectField,
-  SelectFieldState,
-} from "../types";
+import { GetFinalVariablesFunction, SelectField, SelectFieldState } from "../types";
 import Selector from "../../Selector";
 import styled from "styled-components";
 import useFormField from "../hooks/useFormField";

+ 1 - 1
dashboard/src/components/porter-form/field-components/ServiceRow.tsx

@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import styled from "styled-components";
 import { Context } from "shared/Context";
-import { hardcodedNames, hardcodedIcons } from "shared/hardcodedNameDict";
+import { hardcodedIcons, hardcodedNames } from "shared/hardcodedNameDict";
 
 type PropsType = {
   service: {

+ 1 - 5
dashboard/src/components/porter-form/hooks/useFormField.tsx

@@ -1,10 +1,6 @@
 import { useContext, useEffect } from "react";
 import { PorterFormContext } from "../PorterFormContextProvider";
-import {
-  PorterFormFieldFieldState,
-  PorterFormFieldValidationState,
-  PorterFormVariableList,
-} from "../types";
+import { PorterFormFieldFieldState, PorterFormFieldValidationState, PorterFormVariableList } from "../types";
 
 interface FormFieldData<T> {
   state: T;

+ 1 - 1
dashboard/src/components/repo-selector/ActionConfEditor.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useContext } from "react";
+import React from "react";
 import styled from "styled-components";
 
 import { ActionConfigType } from "shared/types";

+ 0 - 2
dashboard/src/components/repo-selector/ActionDetails.tsx

@@ -1,4 +1,3 @@
-import ImageSelector from "components/image-selector/ImageSelector";
 import React, { Component } from "react";
 import styled from "styled-components";
 
@@ -8,7 +7,6 @@ import api from "shared/api";
 import Loading from "components/Loading";
 import { ActionConfigType } from "../../shared/types";
 import InputRow from "../form-components/InputRow";
-import InfoTooltip from "components/InfoTooltip";
 
 type PropsType = {
   actionConfig: ActionConfigType | null;

+ 1 - 1
dashboard/src/components/repo-selector/BranchList.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import branch_icon from "assets/branch.png";
 

+ 1 - 1
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -7,7 +7,7 @@ import close from "assets/close.png";
 
 import api from "../../shared/api";
 import { Context } from "../../shared/Context";
-import { FileType, ActionConfigType } from "../../shared/types";
+import { ActionConfigType, FileType } from "../../shared/types";
 
 import Loading from "../Loading";
 

+ 2 - 3
dashboard/src/components/repo-selector/RepoList.tsx

@@ -1,14 +1,13 @@
-import React, { useState, useContext, useEffect } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import github from "assets/github.png";
 
 import api from "shared/api";
-import { RepoType, ActionConfigType } from "shared/types";
+import { ActionConfigType, RepoType } from "shared/types";
 import { Context } from "shared/Context";
 
 import Loading from "../Loading";
 import SearchBar from "../SearchBar";
-import Helper from "../form-components/Helper";
 
 interface GithubAppAccessData {
   has_access: boolean;

+ 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 { BrowserRouter, Route, Redirect, 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;
-`;

+ 0 - 1
dashboard/src/main/MainWrapper.tsx

@@ -1,5 +1,4 @@
 import React, { Component } from "react";
-import { BrowserRouter } from "react-router-dom";
 
 import { ContextProvider } from "../shared/Context";
 import Main from "./Main";

+ 1 - 1
dashboard/src/main/auth/Register.tsx

@@ -1,4 +1,4 @@
-import React, { ChangeEvent, Component, useContext } from "react";
+import React, { ChangeEvent, Component } from "react";
 import styled from "styled-components";
 import logo from "assets/logo.png";
 import github from "assets/github-icon.png";

+ 1 - 2
dashboard/src/main/auth/VerifyEmail.tsx

@@ -1,9 +1,8 @@
-import React, { ChangeEvent, Component } from "react";
+import React, { Component } from "react";
 import styled from "styled-components";
 import logo from "assets/logo.png";
 
 import api from "shared/api";
-import { emailRegex } from "shared/regex";
 import { Context } from "shared/Context";
 
 type PropsType = {

+ 1 - 1
dashboard/src/main/home/Home.tsx

@@ -5,7 +5,7 @@ import styled from "styled-components";
 import api from "shared/api";
 import { H } from "highlight.run";
 import { Context } from "shared/Context";
-import { PorterUrl, pushQueryParams, pushFiltered } from "shared/routing";
+import { PorterUrl, pushFiltered, pushQueryParams } from "shared/routing";
 import { ClusterType, ProjectType } from "shared/types";
 
 import ConfirmOverlay from "components/ConfirmOverlay";

+ 2 - 7
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -2,16 +2,11 @@ import React, { Component } from "react";
 import styled from "styled-components";
 import monojob from "assets/monojob.png";
 import monoweb from "assets/monoweb.png";
-import { Switch, Route } from "react-router-dom";
+import { Route, Switch } from "react-router-dom";
 
 import { Context } from "shared/Context";
 import { ChartType, ClusterType } from "shared/types";
-import {
-  getQueryParam,
-  PorterUrl,
-  pushFiltered,
-  pushQueryParams,
-} from "shared/routing";
+import { getQueryParam, PorterUrl, pushFiltered, pushQueryParams } from "shared/routing";
 
 import DashboardHeader from "./DashboardHeader";
 import ChartList from "./chart/ChartList";

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -3,7 +3,7 @@ import styled from "styled-components";
 
 import { Context } from "shared/Context";
 import api from "shared/api";
-import { ChartType, StorageType, ClusterType } from "shared/types";
+import { ChartType, ClusterType, StorageType } from "shared/types";
 import { PorterUrl } from "shared/routing";
 
 import Chart from "./Chart";

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx

@@ -1,7 +1,7 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
 
 import Table from "components/Table";
-import { Column, Row } from "react-table";
+import { Column } from "react-table";
 import styled from "styled-components";
 import api from "shared/api";
 import { Context } from "shared/Context";

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx

@@ -1,7 +1,5 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-
-import sliders from "assets/sliders.svg";
 import api from "shared/api";
 
 import { Context } from "shared/Context";

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArray.tsx

@@ -5,7 +5,6 @@ import EnvEditorModal from "main/home/modals/EnvEditorModal";
 
 import sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
-import { keysIn } from "lodash";
 
 export type KeyValueType = {
   key: string;

+ 1 - 4
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -1,19 +1,16 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import close from "assets/close.png";
 import backArrow from "assets/back_arrow.png";
 import key from "assets/key.svg";
-import _ from "lodash";
 import loading from "assets/loading.gif";
 
-import { ChartType, StorageType, ClusterType } from "shared/types";
+import { ClusterType } from "shared/types";
 import { Context } from "shared/Context";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 
 import TitleSection from "components/TitleSection";
 import SaveButton from "components/SaveButton";
-import Loading from "components/Loading";
 import TabRegion from "components/TabRegion";
 import EnvGroupArray, { KeyValueType } from "./EnvGroupArray";
 import Heading from "components/form-components/Heading";

+ 2 - 16
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -1,27 +1,13 @@
-import React, {
-  useContext,
-  useState,
-  useEffect,
-  useRef,
-  useCallback,
-  useMemo,
-} from "react";
+import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import yaml from "js-yaml";
 import backArrow from "assets/back_arrow.png";
 import _ from "lodash";
 import loadingSrc from "assets/loading.gif";
 
-import {
-  ResourceType,
-  ChartType,
-  StorageType,
-  ClusterType,
-} from "shared/types";
+import { ChartType, ClusterType, ResourceType, StorageType } from "shared/types";
 import { Context } from "shared/Context";
 import api from "shared/api";
-
-import Loading from "components/Loading";
 import StatusIndicator from "components/StatusIndicator";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import RevisionSection from "./RevisionSection";

+ 2 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx

@@ -3,14 +3,9 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 import { RouteComponentProps, withRouter } from "react-router";
 
-import {
-  ResourceType,
-  ChartType,
-  StorageType,
-  ClusterType,
-} from "shared/types";
+import { ChartType, StorageType } from "shared/types";
 import api from "shared/api";
-import { PorterUrl, pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import ExpandedJobChart from "./ExpandedJobChart";
 import ExpandedChart from "./ExpandedChart";
 import Loading from "components/Loading";

+ 11 - 32
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -6,17 +6,15 @@ import backArrow from "assets/back_arrow.png";
 import _ from "lodash";
 import loading from "assets/loading.gif";
 
-import { ChartType, StorageType, ClusterType } from "shared/types";
+import { ChartType, ClusterType, StorageType } from "shared/types";
 import { Context } from "shared/Context";
 import api from "shared/api";
 
 import SaveButton from "components/SaveButton";
-import Loading from "components/Loading";
 import TitleSection from "components/TitleSection";
-import JobList from "./jobs/JobList";
+import TempJobList from "./jobs/TempJobList";
 import SettingsSection from "./SettingsSection";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
-import { PlaceHolder } from "brace";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 type PropsType = WithAuthProps & {
@@ -419,25 +417,6 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
   };
 
   renderTabContents = (currentTab: string, submitValues?: any) => {
-    let saveButton = (
-      <ButtonWrapper>
-        <SaveButton
-          onClick={() => this.handleSaveValues(submitValues, true)}
-          status={this.state.saveValuesStatus}
-          makeFlush={true}
-          clearPosition={true}
-          rounded={true}
-          statusPosition="right"
-        >
-          <i className="material-icons">play_arrow</i> Run Job
-        </SaveButton>
-      </ButtonWrapper>
-    );
-
-    if (!this.props.isAuthorized("job", "", ["get", "update", "create"])) {
-      saveButton = null;
-    }
-
     switch (currentTab) {
       case "jobs":
         if (this.state.imageIsPlaceholder) {
@@ -455,12 +434,12 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
         }
         return (
           <TabWrapper>
-            {saveButton}
-            <JobList
+            <TempJobList
+              handleSaveValues={this.handleSaveValues}
               jobs={this.state.jobs}
-              setJobs={(jobs: any) => {
-                this.setState({ jobs });
-              }}
+              setJobs={(jobs: any) => this.setState({ jobs })}
+              isAuthorized={this.props.isAuthorized}
+              saveValuesStatus={this.state.saveValuesStatus}
             />
           </TabWrapper>
         );
@@ -616,14 +595,14 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
                     this.state.imageIsPlaceholder ||
                     !this.props.isAuthorized("job", "", ["get", "update"])
                   }
-                  onSubmit={(formValues) => {
-                    console.log(formValues);
-                    this.handleSaveValues(formValues, false);
-                  }}
+                  onSubmit={(formValues) =>
+                    this.handleSaveValues(formValues, false)
+                  }
                   leftTabOptions={this.state.leftTabOptions}
                   rightTabOptions={this.state.rightTabOptions}
                   saveValuesStatus={this.state.saveValuesStatus}
                   saveButtonText="Save Config"
+                  includeHiddenFields
                 />
               )}
             </BodyWrapper>

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

@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import styled from "styled-components";
 
 import { Context } from "shared/Context";
-import { ResourceType, ChartType } from "shared/types";
+import { ChartType, ResourceType } from "shared/types";
 
 import GraphDisplay from "./graph/GraphDisplay";
 import Loading from "components/Loading";

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

@@ -3,7 +3,7 @@ import styled from "styled-components";
 import yaml from "js-yaml";
 
 import { Context } from "shared/Context";
-import { ResourceType, ChartType } from "shared/types";
+import { ChartType, ResourceType } from "shared/types";
 
 import Loading from "components/Loading";
 import ResourceTab from "components/ResourceTab";

+ 2 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -1,14 +1,9 @@
-import React, { Component, useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import api from "shared/api";
 import yaml from "js-yaml";
 
-import {
-  ChartType,
-  RepoType,
-  StorageType,
-  ActionConfigType,
-} from "shared/types";
+import { ActionConfigType, ChartType, StorageType } from "shared/types";
 import { Context } from "shared/Context";
 
 import ImageSelector from "components/image-selector/ImageSelector";

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx

@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import styled from "styled-components";
 
-import { ResourceType, NodeType, EdgeType, ChartType } from "shared/types";
+import { ChartType, EdgeType, NodeType, ResourceType } from "shared/types";
 
 import Node from "./Node";
 import Edge from "./Edge";

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx

@@ -2,8 +2,8 @@ import React, { Component } from "react";
 import styled from "styled-components";
 import yaml from "js-yaml";
 
-import { kindToIcon, edgeColors } from "shared/rosettaStone";
-import { NodeType, EdgeType } from "shared/types";
+import { edgeColors, kindToIcon } from "shared/rosettaStone";
+import { EdgeType, NodeType } from "shared/types";
 
 import YamlEditor from "components/YamlEditor";
 

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

@@ -2,7 +2,6 @@ import React, { Component } from "react";
 import styled from "styled-components";
 
 import api from "shared/api";
-import _ from "lodash";
 import { Context } from "shared/Context";
 import JobResource from "./JobResource";
 import ConfirmOverlay from "components/ConfirmOverlay";

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

@@ -1,4 +1,4 @@
-import React, { MouseEvent, Component } from "react";
+import React, { Component, MouseEvent } from "react";
 import styled from "styled-components";
 import { Context } from "shared/Context";
 import _ from "lodash";
@@ -7,7 +7,6 @@ import api from "shared/api";
 import Logs from "../status/Logs";
 import plus from "assets/plus.svg";
 import closeRounded from "assets/close-rounded.png";
-import trash from "assets/trash.png";
 import KeyValueArray from "components/form-components/KeyValueArray";
 
 type PropsType = {

+ 55 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/TempJobList.tsx

@@ -0,0 +1,55 @@
+import React, { useContext, useState } from "react";
+import styled from "styled-components";
+
+import { PorterFormContext } from "components/porter-form/PorterFormContextProvider";
+import JobList from "./JobList";
+import SaveButton from "components/SaveButton";
+
+interface Props {
+  isAuthorized: any;
+  saveValuesStatus: string;
+  setJobs: any;
+  jobs: any;
+  handleSaveValues: any;
+}
+
+/**
+ * Temporary functional component for allowing job rerun button to consume
+ * form context (until ExpandedJobChart is migrated to FC)
+ */
+const TempJobList: React.FC<Props> = (props) => {
+  const { getSubmitValues } = useContext(PorterFormContext);
+  const [searchInput, setSearchInput] = useState("");
+
+  let saveButton = (
+    <ButtonWrapper>
+      <SaveButton
+        onClick={() => props.handleSaveValues(getSubmitValues(), true)}
+        status={props.saveValuesStatus}
+        makeFlush={true}
+        clearPosition={true}
+        rounded={true}
+        statusPosition="right"
+      >
+        <i className="material-icons">play_arrow</i> Run Job
+      </SaveButton>
+    </ButtonWrapper>
+  );
+
+  if (!props.isAuthorized("job", "", ["get", "update", "create"])) {
+    saveButton = null;
+  }
+
+  return (
+    <>
+      {saveButton}
+      <JobList jobs={props.jobs} setJobs={props.setJobs} />
+    </>
+  );
+};
+
+export default TempJobList;
+
+const ButtonWrapper = styled.div`
+  margin: 5px 0 35px;
+`;

+ 7 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx

@@ -1,16 +1,16 @@
-import React, { useMemo, useCallback, useRef } from "react";
-import { AreaClosed, Line, Bar, LinePath } from "@visx/shape";
+import React, { useCallback, useMemo, useRef } from "react";
+import { AreaClosed, Bar, Line, LinePath } from "@visx/shape";
 import { curveMonotoneX } from "@visx/curve";
-import { scaleTime, scaleLinear } from "@visx/scale";
-import { AxisLeft, AxisBottom } from "@visx/axis";
+import { scaleLinear, scaleTime } from "@visx/scale";
+import { AxisBottom, AxisLeft } from "@visx/axis";
 
-import { TooltipWithBounds, defaultStyles, useTooltip } from "@visx/tooltip";
+import { defaultStyles, TooltipWithBounds, useTooltip } from "@visx/tooltip";
 
-import { GridRows, GridColumns } from "@visx/grid";
+import { GridColumns, GridRows } from "@visx/grid";
 
 import { localPoint } from "@visx/event";
 import { LinearGradient } from "@visx/gradient";
-import { max, extent, bisector } from "d3-array";
+import { bisector, extent, max } from "d3-array";
 import { timeFormat } from "d3-time-format";
 import { NormalizedMetricsData } from "./types";
 

+ 6 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts

@@ -1,12 +1,15 @@
 import {
+  AvailableMetrics,
   GenericMetricResponse,
-  NormalizedMetricsData,
-  MetricsMemoryDataResponse,
   MetricsCPUDataResponse,
+  MetricsHpaReplicasDataResponse,
+  MetricsMemoryDataResponse,
   MetricsNetworkDataResponse,
   MetricsNGINXErrorsDataResponse,
   AvailableMetrics,
-  MetricsHpaReplicasDataResponse, MetricsNGINXLatencyDataResponse
+  MetricsHpaReplicasDataResponse, 
+  MetricsNGINXLatencyDataResponse
+  NormalizedMetricsData,
 } from "./types";
 
 /**

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

@@ -1,4 +1,4 @@
-import React, { Component, useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 
 import api from "shared/api";

+ 1 - 5
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -3,11 +3,7 @@ import styled from "styled-components";
 
 import { Context } from "shared/Context";
 import api from "shared/api";
-import {
-  ClusterType,
-  DetailedClusterType,
-  DetailedIngressError,
-} from "shared/types";
+import { ClusterType, DetailedClusterType, DetailedIngressError } from "shared/types";
 import Helper from "components/form-components/Helper";
 import { pushFiltered } from "shared/routing";
 

+ 1 - 1
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -14,7 +14,7 @@ import Provisioner from "../provisioner/Provisioner";
 import FormDebugger from "components/porter-form/FormDebugger";
 import TitleSection from "components/TitleSection";
 
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered, pushQueryParams } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 type PropsType = RouteComponentProps &

+ 1 - 3
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -1,6 +1,5 @@
-import React, { useEffect, useContext, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
-import GHIcon from "assets/GithubIcon";
 
 import { Context } from "shared/Context";
 import { integrationList } from "shared/common";
@@ -9,7 +8,6 @@ import IntegrationList from "./IntegrationList";
 import api from "shared/api";
 import { pushFiltered } from "shared/routing";
 import Loading from "../../../components/Loading";
-import ConfirmOverlay from "../../../components/ConfirmOverlay";
 import SlackIntegrationList from "./SlackIntegrationList";
 import TitleSection from "components/TitleSection";
 

+ 1 - 1
dashboard/src/main/home/integrations/Integrations.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from "react";
+import React from "react";
 import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
 
 import { integrationList } from "shared/common";

+ 1 - 1
dashboard/src/main/home/integrations/SlackIntegrationList.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useRef, useContext } from "react";
+import React, { useContext, useRef, useState } from "react";
 import ConfirmOverlay from "../../../components/ConfirmOverlay";
 import styled from "styled-components";
 import { Context } from "../../../shared/Context";

+ 3 - 11
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -13,13 +13,7 @@ import SourcePage from "./SourcePage";
 import SettingsPage from "./SettingsPage";
 import TitleSection from "components/TitleSection";
 
-import {
-  PorterTemplate,
-  ActionConfigType,
-  ChoiceType,
-  ClusterType,
-  StorageType,
-} from "shared/types";
+import { ActionConfigType, PorterTemplate, StorageType } from "shared/types";
 
 type PropsType = RouteComponentProps & {
   currentTab?: string;
@@ -79,7 +73,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
     selectedRegistry: null as any,
   };
 
-  createGHAction = (chartName: string, chartNamespace: string, env?: any) => {
+  createGHAction = (chartName: string, chartNamespace: string) => {
     let { currentProject, currentCluster, setCurrentError } = this.context;
     let {
       actionConfig,
@@ -106,7 +100,6 @@ class LaunchFlow extends Component<PropsType, StateType> {
           folder_path: folderPath,
           image_repo_uri: imageRepoUri,
           git_repo_id: actionConfig.git_repo_id,
-          env: env,
         },
         {
           project_id: currentProject.id,
@@ -320,8 +313,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
       )
       .then((res: any) => {
         if (sourceType === "repo") {
-          let env = rawValues["container.env.normal"];
-          this.createGHAction(name, selectedNamespace, env);
+          this.createGHAction(name, selectedNamespace);
         }
         // this.props.setCurrentView('cluster-dashboard');
         this.setState({ saveValuesStatus: "successful" }, () => {

+ 1 - 6
dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx

@@ -4,12 +4,7 @@ import api from "shared/api";
 
 import { Context } from "shared/Context";
 
-import {
-  ActionConfigType,
-  ChoiceType,
-  ClusterType,
-  StorageType,
-} from "shared/types";
+import { ChoiceType, ClusterType } from "shared/types";
 
 import { isAlphanumeric } from "shared/common";
 

+ 1 - 1
dashboard/src/main/home/modals/DeleteNamespaceModal.tsx

@@ -1,4 +1,4 @@
-import React, { Component, useContext, useMemo, useState } from "react";
+import React, { useContext, useState } from "react";
 import styled from "styled-components";
 import close from "assets/close.png";
 

+ 1 - 1
dashboard/src/main/home/modals/EnvEditorModal.tsx

@@ -1,4 +1,4 @@
-import React, { Component, createRef } from "react";
+import React, { Component } from "react";
 import styled from "styled-components";
 import close from "assets/close.png";
 import AceEditor from "react-ace";

+ 1 - 4
dashboard/src/main/home/modals/LoadEnvGroupModal.tsx

@@ -9,10 +9,7 @@ import { Context } from "shared/Context";
 import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import { KeyValue } from "components/form-components/KeyValueArray";
-import {
-  EnvGroupData,
-  formattedEnvironmentValue,
-} from "../cluster-dashboard/env-groups/EnvGroup";
+import { EnvGroupData, formattedEnvironmentValue } from "../cluster-dashboard/env-groups/EnvGroup";
 
 type PropsType = {
   namespace: string;

+ 1 - 1
dashboard/src/main/home/modals/UpgradeChartModal.tsx

@@ -1,4 +1,4 @@
-import React, { Component, createRef } from "react";
+import React, { Component } from "react";
 import styled from "styled-components";
 import close from "assets/close.png";
 import api from "shared/api";

+ 1 - 4
dashboard/src/main/home/navbar/Navbar.tsx

@@ -1,13 +1,10 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-
-import api from "shared/api";
 import { Context } from "shared/Context";
 
 import Feedback from "./Feedback";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
-import { Select, MenuItem } from "@material-ui/core";
-import { AuthContext } from "shared/auth/AuthContext";
+import { Select } from "@material-ui/core";
 
 type PropsType = WithAuthProps & {
   logOut: () => void;

+ 1 - 7
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -1,10 +1,4 @@
-import React, {
-  Component,
-  useState,
-  useEffect,
-  useContext,
-  useMemo,
-} from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 
 import { InviteType } from "shared/types";

+ 2 - 2
dashboard/src/main/home/provisioner/AWSFormSection.tsx

@@ -5,8 +5,8 @@ import close from "assets/close.png";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { InfraType, ProjectType } from "shared/types";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { InfraType } from "shared/types";
+import { pushFiltered } from "shared/routing";
 
 import SelectRow from "components/form-components/SelectRow";
 import InputRow from "components/form-components/InputRow";

+ 1 - 2
dashboard/src/main/home/provisioner/DOFormSection.tsx

@@ -5,8 +5,7 @@ import close from "assets/close.png";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { InfraType, ProjectType } from "shared/types";
-import { pushQueryParams } from "shared/routing";
+import { InfraType } from "shared/types";
 
 import InputRow from "components/form-components/InputRow";
 import CheckboxRow from "components/form-components/CheckboxRow";

+ 1 - 1
dashboard/src/main/home/provisioner/ExistingClusterSection.tsx

@@ -5,7 +5,7 @@ import api from "shared/api";
 import { ProjectType } from "shared/types";
 import { isAlphanumeric } from "shared/common";
 import { Context } from "shared/Context";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 
 import SaveButton from "components/SaveButton";
 import { RouteComponentProps, withRouter } from "react-router";

+ 2 - 2
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -5,8 +5,8 @@ import close from "assets/close.png";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { InfraType, ProjectType } from "shared/types";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { InfraType } from "shared/types";
+import { pushFiltered } from "shared/routing";
 
 import UploadArea from "components/form-components/UploadArea";
 import SelectRow from "components/form-components/SelectRow";

+ 0 - 2
dashboard/src/main/home/provisioner/Provisioner.tsx

@@ -9,8 +9,6 @@ import Loading from "components/Loading";
 import InfraStatuses from "./InfraStatuses";
 import ProvisionerLogs from "./ProvisionerLogs";
 import { RouteComponentProps, withRouter } from "react-router";
-import { stringify } from "qs";
-import { forEach } from "lodash";
 
 type PropsType = RouteComponentProps & {
   setRefreshClusters: (x: boolean) => void;

+ 0 - 1
dashboard/src/main/home/provisioner/ProvisionerLogs.tsx

@@ -6,7 +6,6 @@ import { RouteComponentProps, withRouter } from "react-router";
 
 import ansiparse from "shared/ansiparser";
 import loading from "assets/loading.gif";
-import warning from "assets/warning.png";
 
 type PropsType = RouteComponentProps & {
   selectedInfra: InfraType;

+ 1 - 1
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -4,7 +4,7 @@ import gradient from "assets/gradient.png";
 
 import { Context } from "shared/Context";
 import { ProjectType } from "shared/types";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import { RouteComponentProps, withRouter } from "react-router";
 
 type PropsType = RouteComponentProps & {

+ 0 - 1
dashboard/src/main/home/sidebar/ProjectSectionContainer.tsx

@@ -1,5 +1,4 @@
 import React, { Component } from "react";
-import styled from "styled-components";
 
 import { Context } from "shared/Context";
 import ProjectSection from "./ProjectSection";

+ 1 - 2
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -13,9 +13,8 @@ import { Context } from "shared/Context";
 
 import ClusterSection from "./ClusterSection";
 import ProjectSectionContainer from "./ProjectSectionContainer";
-import loading from "assets/loading.gif";
 import { RouteComponentProps, withRouter } from "react-router";
-import { pushFiltered, pushQueryParams } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 type PropsType = RouteComponentProps &

+ 1 - 6
dashboard/src/shared/Context.tsx

@@ -1,11 +1,6 @@
 import React, { Component } from "react";
 
-import {
-  ProjectType,
-  ClusterType,
-  CapabilityType,
-  ContextProps,
-} from "shared/types";
+import { CapabilityType, ClusterType, ContextProps, ProjectType } from "shared/types";
 
 import { pushQueryParams } from "shared/routing";
 

+ 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;

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

@@ -122,7 +122,6 @@ const createGHAction = baseApi<
     dockerfile_path: string;
     folder_path: string;
     git_repo_id: number;
-    env: any;
   },
   {
     project_id: number;

+ 1 - 2
dashboard/src/shared/auth/AuthorizationHoc.tsx

@@ -1,5 +1,4 @@
-import React, { useCallback } from "react";
-import { useContext } from "react";
+import React, { useCallback, useContext } from "react";
 import { AuthContext } from "./AuthContext";
 import { isAuthorized } from "./authorization-helpers";
 import { ScopeType, Verbs } from "./types";

+ 2 - 2
dashboard/src/shared/auth/RouteGuard.tsx

@@ -1,6 +1,6 @@
 import UnauthorizedPage from "components/UnauthorizedPage";
-import React, { useMemo, useContext } from "react";
-import { Redirect, Route, RouteProps } from "react-router";
+import React, { useContext, useMemo } from "react";
+import { Route, RouteProps } from "react-router";
 import { AuthContext } from "./AuthContext";
 import { isAuthorized } from "./authorization-helpers";
 import { ScopeType, Verbs } from "./types";

+ 0 - 1
dashboard/src/shared/common.tsx

@@ -2,7 +2,6 @@ import aws from "../assets/aws.png";
 import digitalOcean from "../assets/do.png";
 import gcp from "../assets/gcp.png";
 import github from "../assets/github.png";
-import { InfraType } from "../shared/types";
 
 export const infraNames: any = {
   ecr: "Elastic Container Registry (ECR)",

+ 0 - 2
dashboard/src/shared/routing.tsx

@@ -1,5 +1,3 @@
-import { Location } from "history";
-
 export type PorterUrl =
   | "dashboard"
   | "launch"

+ 21 - 9
docs/guides/running-porter-locally.md

@@ -1,19 +1,31 @@
-While it requires a few additional steps, it is possible to run Porter locally. These are the steps to start using Porter on your local machine.
+While it requires a few additional steps, it is possible to run Porter locally. Porter can either be run inside a Docker container, or the binary can be run directly.
+
+## Running the Binary
+
+To run the Porter binary, follow these steps: 
 
 1. [Install our CLI](https://docs.getporter.dev/docs/cli-documentation#installation)
 
 2. Run `porter server start`. This will spin up a local Porter instance on port 8080.
 
-By default, GitHub login and the deploying from GitHub repo is disabled on the local version of Porter - this is due to security reasons. However, you can add these functionalities to your local instance by creating your own GitHub OAuth application. These are the steps to enable the GitHub features on the local version of Porter:
+3. Navigate to http://localhost:8080/register, and create a new user with an email and password. 
 
-1. [Create a new GitHub Oauth App](https://docs.github.com/en/developers/apps/creating-an-oauth-app). This app should be created with `http://localhost:8080/api/oauth/github/callback` as the callback URL. 
+## Running with Docker
 
-2. Copy the Client ID and the Client secrets. Then add these lines into your `.bashrc` file:
+The easiest way to run the Docker container is to use SQLite as the persistence option. To accomplish this, you can simply run:
 
-```txt
-export GITHUB_CLIENT_ID=YOUR_CLIENT_ID
-export GITHUB_CLIENT_SECRET=YOUR_CLIENT_SECRET
-export GITHUB_ENABLED=true
 ```
+docker volume create porter_sqlite
+docker run \
+  --mount type=volume,source=porter_sqlite,target=/sqlite,readonly=false \
+  -e REDIS_ENABLED=false \
+  -e SQL_LITE_PATH=/sqlite/porter.db \
+  -p 8080:8080 \
+  -d porter1/porter:latest
+```
+
+Then navigate to http://localhost:8080/register, and create a new user with an email and password. 
+
+## Setting up Integrations
 
-3. When you run `porter server start`, Porter will automatically read these variables in and enable the GitHub features.
+While basic functionality is supported on the local binary/Docker image, more configuration is required to support various integrations. See [this document](https://docs.porter.run/docs/sso) for instructions on adding integrations like Github application access.

+ 25 - 0
docs/guides/slack-integration.md

@@ -0,0 +1,25 @@
+# Enabling Slack Integrations
+
+For order to set up a Slack integration on a self-hosted version of Porter, you must create a new Slack app in your workspace for sending Porter notifications. 
+
+## Step 1: Create Application and Environment Variables
+
+Navigate to [https://api.slack.com/apps](https://api.slack.com/apps), and click **Create New App**. On the modal that pops up, select **From Scratch** and then enter your app name and workspace you want to develop in. On the page for the application, scroll down to **App Credentials** and take note of the following two values:
+
+<img width="689" alt="Screen Shot 2021-08-09 at 10 25 41 AM" src="https://user-images.githubusercontent.com/25856165/128722685-28bd99c5-3a28-43cb-b002-356f6963a682.png">
+
+Copy these values into the following environment variables in your installation:
+
+```
+SLACK_CLIENT_ID=<client-id-above>
+SLACK_CLIENT_SECRET=<client-secret-above>
+```
+
+## Step 2: Setting up OAuth
+
+The app also needs to be able to perform the OAuth flow with the right callback link. To do this, 
+navigate to **OAuth & Permissions** in the Slack developer settings and add the url `https://yourdomain.com/api/oauth/slack/callback` to the list of redirect URLs:
+
+![image](https://user-images.githubusercontent.com/25856165/128723683-c4fb2ac4-e0df-4989-9224-08806aadcb26.png)
+
+That's it! You can now follow [this guide](https://docs.porter.run/docs/setting-up-slack-notifications) for setting up Slack notifications in Porter. 

+ 10 - 10
internal/forms/git_action.go

@@ -7,19 +7,18 @@ import (
 // CreateGitAction represents the accepted values for creating a
 // github action integration
 type CreateGitAction struct {
-	ReleaseID      uint              `json:"release_id" form:"required"`
-	GitRepo        string            `json:"git_repo" form:"required"`
-	GitBranch      string            `json:"git_branch"`
-	ImageRepoURI   string            `json:"image_repo_uri" form:"required"`
-	DockerfilePath string            `json:"dockerfile_path"`
-	FolderPath     string            `json:"folder_path"`
-	GitRepoID      uint              `json:"git_repo_id" form:"required"`
-	BuildEnv       map[string]string `json:"env"`
-	RegistryID     uint              `json:"registry_id"`
+	ReleaseID      uint   `json:"release_id" form:"required"`
+	GitRepo        string `json:"git_repo" form:"required"`
+	GitBranch      string `json:"git_branch"`
+	ImageRepoURI   string `json:"image_repo_uri" form:"required"`
+	DockerfilePath string `json:"dockerfile_path"`
+	FolderPath     string `json:"folder_path"`
+	GitRepoID      uint   `json:"git_repo_id" form:"required"`
+	RegistryID     uint   `json:"registry_id"`
 }
 
 // ToGitActionConfig converts the form to a gorm git action config model
-func (ca *CreateGitAction) ToGitActionConfig() (*models.GitActionConfig, error) {
+func (ca *CreateGitAction) ToGitActionConfig(version string) (*models.GitActionConfig, error) {
 	return &models.GitActionConfig{
 		ReleaseID:            ca.ReleaseID,
 		GitRepo:              ca.GitRepo,
@@ -29,6 +28,7 @@ func (ca *CreateGitAction) ToGitActionConfig() (*models.GitActionConfig, error)
 		FolderPath:           ca.FolderPath,
 		GithubInstallationID: ca.GitRepoID,
 		IsInstallation:       true,
+		Version:              version,
 	}, nil
 }
 

+ 12 - 27
internal/integrations/ci/actions/actions.go

@@ -29,14 +29,14 @@ type GithubActions struct {
 
 	GithubConf           *oauth2.Config // one of these will let us authenticate
 	GithubAppID          int64
+	GithubAppSecretPath  string
 	GithubInstallationID uint
 
-	WebhookToken string
-	PorterToken  string
-	BuildEnv     map[string]string
-	ProjectID    uint
-	ClusterID    uint
-	ReleaseName  string
+	PorterToken string
+	BuildEnv    map[string]string
+	ProjectID   uint
+	ClusterID   uint
+	ReleaseName string
 
 	GitBranch      string
 	DockerFilePath string
@@ -44,6 +44,7 @@ type GithubActions struct {
 	ImageRepoURL   string
 
 	defaultBranch string
+	Version       string
 }
 
 func (g *GithubActions) Setup() (string, error) {
@@ -66,24 +67,8 @@ func (g *GithubActions) Setup() (string, error) {
 
 	g.defaultBranch = repo.GetDefaultBranch()
 
-	// create a new secret with a webhook token
-	err = g.createGithubSecret(client, g.getWebhookSecretName(), g.WebhookToken)
-
-	if err != nil {
-		return "", err
-	}
-
-	// create new secrets porter token, project id, and cluster id
-	err = g.createGithubSecret(client, g.getPorterTokenSecretName(), g.PorterToken)
-
-	if err != nil {
-		return "", err
-	}
-
-	// create a new secret with the build variables
-	err = g.createEnvSecret(client)
-
-	if err != nil {
+	// create porter token secret
+	if err := g.createGithubSecret(client, g.getPorterTokenSecretName(), g.PorterToken); err != nil {
 		return "", err
 	}
 
@@ -139,6 +124,7 @@ type GithubActionYAMLStep struct {
 	Timeout uint64            `yaml:"timeout-minutes,omitempty"`
 	Uses    string            `yaml:"uses,omitempty"`
 	Run     string            `yaml:"run,omitempty"`
+	With    map[string]string `yaml:"with,omitempty"`
 	Env     map[string]string `yaml:"env,omitempty"`
 }
 
@@ -166,8 +152,7 @@ type GithubActionYAML struct {
 func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 	gaSteps := []GithubActionYAMLStep{
 		getCheckoutCodeStep(),
-		getDownloadPorterStep(),
-		getConfigurePorterStep(g.ServerURL, g.getPorterTokenSecretName(), g.ProjectID, g.ClusterID, g.ReleaseName),
+		getUpdateAppStep(g.ServerURL, g.getPorterTokenSecretName(), g.ProjectID, g.ClusterID, g.ReleaseName, g.Version),
 	}
 
 	branch := g.GitBranch
@@ -229,7 +214,7 @@ func (g *GithubActions) getClient() (*github.Client, error) {
 		http.DefaultTransport,
 		g.GithubAppID,
 		int64(g.GithubInstallationID),
-		"/porter/docker/github_app_private_key.pem")
+		g.GithubAppSecretPath)
 
 	if err != nil {
 		return nil, err

+ 12 - 29
internal/integrations/ci/actions/steps.go

@@ -4,6 +4,8 @@ import (
 	"fmt"
 )
 
+const updateAppActionName = "porter-dev/porter-update-action"
+
 func getCheckoutCodeStep() GithubActionYAMLStep {
 	return GithubActionYAMLStep{
 		Name: "Checkout code",
@@ -11,36 +13,17 @@ func getCheckoutCodeStep() GithubActionYAMLStep {
 	}
 }
 
-const download string = `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
-unzip -a $name
-rm $name
-chmod +x ./porter
-sudo mv ./porter /usr/local/bin/porter
-`
-
-func getDownloadPorterStep() GithubActionYAMLStep {
+func getUpdateAppStep(serverURL, porterTokenSecretName string, projectID uint, clusterID uint, appName string, actionVersion string) GithubActionYAMLStep {
 	return GithubActionYAMLStep{
-		Name: "Download Porter",
-		ID:   "download_porter",
-		Run:  download,
-	}
-}
-
-const configure string = `porter update --app %s`
-
-func getConfigurePorterStep(serverURL, porterTokenSecretName string, projectID uint, clusterID uint, appName string) GithubActionYAMLStep {
-	return GithubActionYAMLStep{
-		Name:    "Update Porter App",
-		ID:      "update_porter",
-		Run:     fmt.Sprintf(configure, appName),
-		Timeout: 20,
-		Env: map[string]string{
-			"PORTER_TOKEN":   fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
-			"PORTER_HOST":    serverURL,
-			"PORTER_PROJECT": fmt.Sprintf("%d", projectID),
-			"PORTER_CLUSTER": fmt.Sprintf("%d", clusterID),
+		Name: "Update Porter App",
+		Uses: fmt.Sprintf("%s@%s", updateAppActionName, actionVersion),
+		With: map[string]string{
+			"app":     appName,
+			"cluster": fmt.Sprintf("%d", clusterID),
+			"host":    serverURL,
+			"project": fmt.Sprintf("%d", projectID),
+			"token":   fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
 		},
+		Timeout: 20,
 	}
 }

+ 131 - 57
internal/integrations/slack/notifier.go

@@ -2,8 +2,10 @@ package slack
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"net/http"
+	"strings"
 	"time"
 
 	"github.com/porter-dev/porter/internal/models/integrations"
@@ -58,80 +60,152 @@ func NewSlackNotifier(slackInts ...*integrations.SlackIntegration) Notifier {
 	}
 }
 
+type SlackPayload struct {
+	Blocks []*SlackBlock `json:"blocks"`
+}
+
+type SlackBlock struct {
+	Type string     `json:"type"`
+	Text *SlackText `json:"text,omitempty"`
+}
+
+type SlackText struct {
+	Type string `json:"type"`
+	Text string `json:"text"`
+}
+
 func (s *SlackNotifier) Notify(opts *NotifyOpts) error {
-	var statusPayload string
+	blocks := []*SlackBlock{
+		getMessageBlock(opts),
+		getDividerBlock(),
+		getMarkdownBlock(fmt.Sprintf("*Name:* %s", "`"+opts.Name+"`")),
+		getMarkdownBlock(fmt.Sprintf("*Namespace:* %s", "`"+opts.Namespace+"`")),
+		getMarkdownBlock(fmt.Sprintf("*Version:* %d", opts.Version)),
+	}
 
-	switch opts.Status {
-	case StatusDeployed:
-		statusPayload = getSuccessPayload(opts)
-	case StatusFailed:
-		statusPayload = getFailedPayload(opts)
+	// we create a basic payload as a fallback if the detailed payload with "info" fails, due to
+	// marshaling errors on the Slack API side.
+	basicSlackPayload := &SlackPayload{
+		Blocks: blocks,
 	}
 
-	payload := fmt.Sprintf(`
-	{
-		"blocks": [
-			%s
-			{
-				"type": "divider"
-			},
-			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "*Name:* %s"
-				}
-			},
-			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "*Namespace:* %s"
-				}
-			},
-			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "*Version:* %d"
-				}
-			}
-		]
+	infoBlock := getInfoBlock(opts)
+
+	if infoBlock != nil {
+		blocks = append(blocks, infoBlock)
 	}
-	`, statusPayload, "`"+opts.Name+"`", "`"+opts.Namespace+"`", opts.Version)
 
-	reqBody := bytes.NewReader([]byte(payload))
+	slackPayload := &SlackPayload{
+		Blocks: blocks,
+	}
+
+	basicPayload, err := json.Marshal(basicSlackPayload)
+
+	if err != nil {
+		return err
+	}
+
+	payload, err := json.Marshal(slackPayload)
+
+	if err != nil {
+		return err
+	}
+
+	basicReqBody := bytes.NewReader(basicPayload)
+	reqBody := bytes.NewReader(payload)
 	client := &http.Client{
 		Timeout: time.Second * 5,
 	}
 
 	for _, slackInt := range s.slackInts {
-		client.Post(string(slackInt.Webhook), "application/json", reqBody)
+		resp, err := client.Post(string(slackInt.Webhook), "application/json", reqBody)
+
+		if err != nil || resp.StatusCode != 200 {
+			client.Post(string(slackInt.Webhook), "application/json", basicReqBody)
+		}
 	}
 
 	return nil
 }
 
-func getSuccessPayload(opts *NotifyOpts) string {
-	return fmt.Sprintf(`
-		{
-			"type": "section",
-			"text": {
-				"type": "mrkdwn",
-				"text": ":rocket: Your application %s was successfully updated on Porter! <%s|View the new release.>"
-			}
-		},
-	`, "`"+opts.Name+"`", opts.URL)
+func getDividerBlock() *SlackBlock {
+	return &SlackBlock{
+		Type: "divider",
+	}
 }
 
-func getFailedPayload(opts *NotifyOpts) string {
-	return fmt.Sprintf(`
-		{
-			"type": "section",
-			"text": {
-				"type": "mrkdwn",
-				"text": ":x: Your application %s failed to deploy on Porter. <%s|View the status here.>"
-			}
+func getMarkdownBlock(md string) *SlackBlock {
+	return &SlackBlock{
+		Type: "section",
+		Text: &SlackText{
+			Type: "mrkdwn",
+			Text: md,
 		},
-	`, "`"+opts.Name+"`", opts.URL)
+	}
+}
+
+func getMessageBlock(opts *NotifyOpts) *SlackBlock {
+	var md string
+
+	switch opts.Status {
+	case StatusDeployed:
+		md = getSuccessMessage(opts)
+	case StatusFailed:
+		md = getFailedMessage(opts)
+	}
+
+	return getMarkdownBlock(md)
+}
+
+func getInfoBlock(opts *NotifyOpts) *SlackBlock {
+	var md string
+
+	switch opts.Status {
+	case StatusFailed:
+		md = getFailedInfoMessage(opts)
+	default:
+		return nil
+	}
+
+	return getMarkdownBlock(md)
+}
+
+func getSuccessMessage(opts *NotifyOpts) string {
+	return fmt.Sprintf(
+		":rocket: Your application %s was successfully updated on Porter! <%s|View the new release.>",
+		"`"+opts.Name+"`",
+		opts.URL,
+	)
+}
+
+func getFailedMessage(opts *NotifyOpts) string {
+	return fmt.Sprintf(
+		":x: Your application %s failed to deploy on Porter. <%s|View the status here.>",
+		"`"+opts.Name+"`",
+		opts.URL,
+	)
+}
+
+func getFailedInfoMessage(opts *NotifyOpts) string {
+	info := opts.Info
+
+	// TODO: this casing is quite ugly and looks for particular types of API server
+	// errors, otherwise it truncates the error message to 200 characters. This should
+	// handle the errors more gracefully.
+	if strings.Contains(info, "Invalid value:") {
+		errArr := strings.Split(info, "Invalid value:")
+
+		// look for "unmarshalerDecoder" error
+		if strings.Contains(info, "unmarshalerDecoder") {
+			udArr := strings.Split(info, "unmarshalerDecoder:")
+
+			info = errArr[0] + udArr[1]
+		} else {
+			info = errArr[0] + "..."
+		}
+	} else if len(info) > 200 {
+		info = info[0:200] + "..."
+	}
+
+	return fmt.Sprintf("```\n%s\n```", info)
 }

+ 1 - 1
internal/kubernetes/config.go

@@ -312,7 +312,7 @@ func (conf *OutOfClusterConfig) CreateRawConfigFromCluster() (*api.Config, error
 		}
 
 		// add this as a bearer token
-		authInfoMap[authInfoName].Token = tok
+		authInfoMap[authInfoName].Token = tok.AccessToken
 	case models.AWS:
 		awsAuth, err := conf.Repo.AWSIntegration.ReadAWSIntegration(
 			cluster.AWSIntegrationID,

+ 2 - 0
internal/models/gitrepo.go

@@ -75,6 +75,8 @@ type GitActionConfig struct {
 
 	// Determines on how authentication is performed on this action
 	IsInstallation bool `json:"is_installation"`
+
+	Version string `json:"version" gorm:"default:v0.0.1"`
 }
 
 // GitActionConfigExternal is an external GitActionConfig to be shared over REST

+ 9 - 5
internal/models/integrations/gcp.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 
+	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
 	"gorm.io/gorm"
 )
@@ -83,13 +84,16 @@ func (g *GCPIntegration) GetBearerToken(
 	getTokenCache GetTokenCacheFunc,
 	setTokenCache SetTokenCacheFunc,
 	scopes ...string,
-) (string, error) {
+) (*oauth2.Token, error) {
 	cache, err := getTokenCache()
 
 	// check the token cache for a non-expired token
 	if cache != nil {
 		if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 {
-			return string(tok), nil
+			return &oauth2.Token{
+				AccessToken: string(cache.Token),
+				Expiry:      cache.Expiry,
+			}, nil
 		}
 	}
 
@@ -100,19 +104,19 @@ func (g *GCPIntegration) GetBearerToken(
 	)
 
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 
 	tok, err := creds.TokenSource.Token()
 
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 
 	// update the token cache
 	setTokenCache(tok.AccessToken, tok.Expiry)
 
-	return tok.AccessToken, nil
+	return tok, nil
 }
 
 // credentialsFile is the unmarshalled representation of a GCP credentials file.

+ 16 - 7
internal/registry/registry.go

@@ -92,6 +92,8 @@ type gcrRepositoryResp struct {
 }
 
 func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, error) {
+	getTokenCache := r.getTokenCacheFunc(repo)
+
 	gcp, err := repo.GCPIntegration.ReadGCPIntegration(
 		r.GCPIntegrationID,
 	)
@@ -102,7 +104,7 @@ func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, er
 
 	// get oauth2 access token
 	_, err = gcp.GetBearerToken(
-		r.getTokenCache,
+		getTokenCache,
 		r.setTokenCacheFunc(repo),
 		"https://www.googleapis.com/auth/devstorage.read_write",
 	)
@@ -112,7 +114,7 @@ func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, er
 	}
 
 	// it's now written to the token cache, so return
-	cache, err := r.getTokenCache()
+	cache, err := getTokenCache()
 
 	if err != nil {
 		return nil, err
@@ -352,11 +354,18 @@ func (r *Registry) listPrivateRegistryRepositories(
 	return res, nil
 }
 
-func (r *Registry) getTokenCache() (tok *ints.TokenCache, err error) {
-	return &ints.TokenCache{
-		Token:  r.TokenCache.Token,
-		Expiry: r.TokenCache.Expiry,
-	}, nil
+func (r *Registry) getTokenCacheFunc(
+	repo repository.Repository,
+) ints.GetTokenCacheFunc {
+	return func() (tok *ints.TokenCache, err error) {
+		reg, err := repo.Registry.ReadRegistry(r.ID)
+
+		if err != nil {
+			return nil, err
+		}
+
+		return &reg.TokenCache.TokenCache, nil
+	}
 }
 
 func (r *Registry) setTokenCacheFunc(

Некоторые файлы не были показаны из-за большого количества измененных файлов