Przeglądaj źródła

Merge branch 'master' into 0.7.0-better-metrics

Ivan Galakhov 4 lat temu
rodzic
commit
46dabda7a3
100 zmienionych plików z 829 dodań i 662 usunięć
  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: |
         run: |
           cat >./dashboard/.env <<EOL
           cat >./dashboard/.env <<EOL
           NODE_ENV=production
           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
           EOL
 
 
           cat ./dashboard/.env
           cat ./dashboard/.env
@@ -62,13 +59,8 @@ jobs:
         run: |
         run: |
           cat >./dashboard/.env <<EOL
           cat >./dashboard/.env <<EOL
           NODE_ENV=production
           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
           EOL
       - name: Build and zip static folder
       - name: Build and zip static folder
         run: |
         run: |
@@ -355,3 +347,38 @@ jobs:
           asset_path: ./release/static/static_${{steps.tag_name.outputs.tag}}.zip
           asset_path: ./release/static/static_${{steps.tag_name.outputs.tag}}.zip
           asset_name: static_${{steps.tag_name.outputs.tag}}.zip
           asset_name: static_${{steps.tag_name.outputs.tag}}.zip
           asset_content_type: application/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
 // 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
 // ListGitRepos returns a list of Git repos for a project
 func (c *Client) ListGitRepos(
 func (c *Client) ListGitRepos(

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

@@ -11,15 +11,14 @@ import (
 // CreateGithubActionRequest represents the accepted fields for creating
 // CreateGithubActionRequest represents the accepted fields for creating
 // a Github action
 // a Github action
 type CreateGithubActionRequest struct {
 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
 // 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{
 var connectGCRCmd = &cobra.Command{
 	Use:   "gcr",
 	Use:   "gcr",
 	Short: "Adds a GCR instance to a project",
 	Short: "Adds a GCR instance to a project",
@@ -135,7 +123,6 @@ func init() {
 		"the context to connect (defaults to the current context)",
 		"the context to connect (defaults to the current context)",
 	)
 	)
 
 
-	connectCmd.AddCommand(connectActionsCmd)
 	connectCmd.AddCommand(connectECRCmd)
 	connectCmd.AddCommand(connectECRCmd)
 	connectCmd.AddCommand(connectRegistryCmd)
 	connectCmd.AddCommand(connectRegistryCmd)
 	connectCmd.AddCommand(connectDockerhubCmd)
 	connectCmd.AddCommand(connectDockerhubCmd)
@@ -243,10 +230,3 @@ func runConnectHelmRepoBasic(_ *api.AuthCheckResponse, client *api.Client, _ []s
 
 
 	return config.SetHelmRepo(hrID)
 	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 values string
 var source string
 var source string
 var image string
 var image string
+var registryURL string
 
 
 func init() {
 func init() {
 	rootCmd.AddCommand(createCmd)
 	rootCmd.AddCommand(createCmd)
@@ -137,6 +138,13 @@ func init() {
 		"",
 		"",
 		"if the source is \"registry\", the image to use, in repository:tag format",
 		"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": ""}
 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],
 			Kind:        args[0],
 			ReleaseName: name,
 			ReleaseName: name,
+			RegistryURL: registryURL,
 		},
 		},
 	}
 	}
 
 

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

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

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

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

+ 6 - 2
cli/cmd/errors.go

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

+ 9 - 2
cli/cmd/logs.go

@@ -13,6 +13,7 @@ import (
 // without any subcommands
 // without any subcommands
 var logsCmd = &cobra.Command{
 var logsCmd = &cobra.Command{
 	Use:   "logs [release]",
 	Use:   "logs [release]",
+	Args:  cobra.ExactArgs(1),
 	Short: "Logs the output from a given application.",
 	Short: "Logs the output from a given application.",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, logs)
 		err := checkLoginAndRun(args, logs)
@@ -96,11 +97,17 @@ func logs(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 		selectedContainerName = selectedContainer
 		selectedContainerName = selectedContainer
 	}
 	}
 
 
-	restConf, err := getRESTConfig(client)
+	config := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err = config.setSharedConfig()
 
 
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		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 namespace string
+var verbose bool
 
 
 // runCmd represents the "porter run" base command when called
 // runCmd represents the "porter run" base command when called
 // without any subcommands
 // without any subcommands
@@ -60,6 +61,14 @@ func init() {
 		false,
 		false,
 		"whether to connect to an existing pod",
 		"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 {
 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 {
 	if len(podsSimple) == 0 {
 		return fmt.Errorf("At least one pod must exist in this deployment.")
 		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]
 		selectedPod = podsSimple[0]
 	} else {
 	} else {
 		podNames := make([]string, 0)
 		podNames := make([]string, 0)
@@ -116,27 +125,38 @@ func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 		selectedContainerName = selectedContainer
 		selectedContainerName = selectedContainer
 	}
 	}
 
 
-	restConf, err := getRESTConfig(client)
+	config := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err = config.setSharedConfig()
 
 
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 	}
 
 
 	if existingPod {
 	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
 	pID := config.Project
 	cID := config.Cluster
 	cID := config.Cluster
 
 
-	kubeResp, err := client.GetKubeconfig(context.TODO(), pID, cID)
+	kubeResp, err := p.Client.GetKubeconfig(context.TODO(), pID, cID)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return err
 	}
 	}
 
 
 	kubeBytes := kubeResp.Kubeconfig
 	kubeBytes := kubeResp.Kubeconfig
@@ -144,13 +164,13 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 	cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
 	cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return err
 	}
 	}
 
 
 	restConf, err := cmdConf.ClientConfig()
 	restConf, err := cmdConf.ClientConfig()
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return err
 	}
 	}
 
 
 	restConf.GroupVersion = &schema.GroupVersion{
 	restConf.GroupVersion = &schema.GroupVersion{
@@ -160,7 +180,25 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 
 
 	restConf.NegotiatedSerializer = runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{})
 	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 {
 type podSimple struct {
@@ -196,14 +234,8 @@ func getPods(client *api.Client, namespace, releaseName string) ([]podSimple, er
 	return res, nil
 	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").
 		Resource("pods").
 		Name(name).
 		Name(name).
 		Namespace(namespace).
 		Namespace(namespace).
@@ -224,7 +256,7 @@ func executeRun(config *rest.Config, namespace, name, container string, args []s
 	}
 	}
 
 
 	fn := func() error {
 	fn := func() error {
-		exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+		exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -242,10 +274,10 @@ func executeRun(config *rest.Config, namespace, name, container string, args []s
 		return err
 		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)
 	existing, err := getExistingPod(config, name, namespace)
 
 
 	if err != nil {
 	if err != nil {
@@ -267,13 +299,7 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 	}
 	}
 
 
 	fn := func() error {
 	fn := func() error {
-		restClient, err := rest.RESTClientFor(config)
-
-		if err != nil {
-			return err
-		}
-
-		req := restClient.Post().
+		req := config.RestClient.Post().
 			Resource("pods").
 			Resource("pods").
 			Name(podName).
 			Name(podName).
 			Namespace("default").
 			Namespace("default").
@@ -284,7 +310,7 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 		req.Param("tty", "true")
 		req.Param("tty", "true")
 		req.Param("container", container)
 		req.Param("container", container)
 
 
-		exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+		exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -309,12 +335,25 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 
 
 		time.Sleep(2 * time.Second)
 		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
 	// delete the ephemeral pod
@@ -323,75 +362,79 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 	return err
 	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{
 	podLogOpts := v1.PodLogOptions{
 		Container: container,
 		Container: container,
 		Follow:    follow,
 		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(
 	podLogs, err := req.Stream(
 		context.Background(),
 		context.Background(),
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return 0, err
 	}
 	}
 
 
 	defer podLogs.Close()
 	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 {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
+	for _, event := range resp.Items {
+		color.New(color.FgRed).Println(event.Message)
+	}
+
 	return nil
 	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(),
 		context.Background(),
 		name,
 		name,
 		metav1.GetOptions{},
 		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(),
 		context.Background(),
 		name,
 		name,
 		metav1.DeleteOptions{},
 		metav1.DeleteOptions{},
 	)
 	)
-}
-
-func createPodFromExisting(config *rest.Config, existing *v1.Pod, args []string) (*v1.Pod, error) {
-	clientset, err := kubernetes.NewForConfig(config)
 
 
 	if err != nil {
 	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()
 	newPod := existing.DeepCopy()
 
 
 	// only copy the pod spec, overwrite metadata
 	// 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{}
 	newPod.Status = v1.PodStatus{}
 
 
+	// only use "primary" container
+	newPod.Spec.Containers = newPod.Spec.Containers[0:1]
+
 	// set restart policy to never
 	// set restart policy to never
 	newPod.Spec.RestartPolicy = v1.RestartPolicyNever
 	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].TTY = true
 	newPod.Spec.Containers[0].Stdin = true
 	newPod.Spec.Containers[0].Stdin = true
 	newPod.Spec.Containers[0].StdinOnce = 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
 	// 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(),
 		context.Background(),
 		newPod,
 		newPod,
 		metav1.CreateOptions{},
 		metav1.CreateOptions{},

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

@@ -11,7 +11,7 @@ func OpenBrowser(url string) error {
 	var cmd string
 	var cmd string
 	var args []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 {
 	switch runtime.GOOS {
 	case "windows":
 	case "windows":

+ 1 - 1
cli/cmd/version.go

@@ -7,7 +7,7 @@ import (
 )
 )
 
 
 // Version will be linked by an ldflag during build
 // 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{
 var versionCmd = &cobra.Command{
 	Use:     "version",
 	Use:     "version",

+ 8 - 0
dashboard/package-lock.json

@@ -6244,6 +6244,14 @@
         "scheduler": "^0.19.1"
         "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": {
     "react-is": {
       "version": "16.13.1",
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "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-router-dom": "^5.2.0",
     "react-table": "^7.7.0",
     "react-table": "^7.7.0",
     "semver": "^7.3.5",
     "semver": "^7.3.5",
-    "styled-components": "^5.2.0"
+    "styled-components": "^5.2.0",
+    "react-error-boundary": "^3.1.3"
   },
   },
   "scripts": {
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "test": "echo \"Error: no test specified\" && exit 1",

+ 41 - 3
dashboard/src/App.tsx

@@ -1,14 +1,52 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
 import { BrowserRouter } from "react-router-dom";
 import { BrowserRouter } from "react-router-dom";
+import PorterErrorBoundary from "shared/PorterErrorBoundary";
+import styled, { createGlobalStyle } from "styled-components";
 
 
 import MainWrapper from "./main/MainWrapper";
 import MainWrapper from "./main/MainWrapper";
 
 
 export default class App extends Component {
 export default class App extends Component {
   render() {
   render() {
     return (
     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 React, { Component, RefObject } from "react";
 import Tooltip from "@material-ui/core/Tooltip";
 import Tooltip from "@material-ui/core/Tooltip";
 import styled from "styled-components";
 import styled from "styled-components";
-import { styled as materialStyled } from "@material-ui/core/styles";
 
 
 type PropsType = {
 type PropsType = {
   text: string;
   text: string;

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

@@ -1,7 +1,6 @@
 import React from "react";
 import React from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import { Column, Row, useGlobalFilter, useTable } from "react-table";
 import { Column, Row, useGlobalFilter, useTable } from "react-table";
-import InputRow from "./form-components/InputRow";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 
 
 const GlobalFilter: React.FunctionComponent<any> = ({ setGlobalFilter }) => {
 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 sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
 import upload from "assets/upload.svg";
-import { keysIn } from "lodash";
 
 
 export type KeyValue = {
 export type KeyValue = {
   key: string;
   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 {
 import {
-  Section,
+  ArrayInputField,
+  CheckboxField,
   FormField,
   FormField,
   InputField,
   InputField,
-  CheckboxField,
   KeyValueArrayField,
   KeyValueArrayField,
-  ArrayInputField,
-  SelectField,
-  ServiceIPListField,
   ResourceListField,
   ResourceListField,
+  Section,
+  SelectField,
+  ServiceIPListField
 } from "./types";
 } from "./types";
 import TabRegion, { TabOption } from "../TabRegion";
 import TabRegion, { TabOption } from "../TabRegion";
 import Heading from "../form-components/Heading";
 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 React, { createContext, useContext, useReducer } from "react";
 import {
 import {
+  GetFinalVariablesFunction,
+  PorterFormAction,
   PorterFormData,
   PorterFormData,
   PorterFormState,
   PorterFormState,
-  PorterFormAction,
-  PorterFormVariableList,
   PorterFormValidationInfo,
   PorterFormValidationInfo,
-  GetFinalVariablesFunction,
+  PorterFormVariableList,
 } from "./types";
 } from "./types";
 import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
 import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
@@ -20,6 +20,7 @@ interface Props {
   onSubmit: (vars: PorterFormVariableList) => void;
   onSubmit: (vars: PorterFormVariableList) => void;
   initialVariables?: PorterFormVariableList;
   initialVariables?: PorterFormVariableList;
   overrideVariables?: PorterFormVariableList;
   overrideVariables?: PorterFormVariableList;
+  includeHiddenFields?: boolean;
   isReadOnly?: boolean;
   isReadOnly?: boolean;
   doDebug?: boolean;
   doDebug?: boolean;
 }
 }
@@ -30,6 +31,7 @@ interface ContextProps {
   onSubmit: () => void;
   onSubmit: () => void;
   dispatchAction: (event: PorterFormAction) => void;
   dispatchAction: (event: PorterFormAction) => void;
   validationInfo: PorterFormValidationInfo;
   validationInfo: PorterFormValidationInfo;
+  getSubmitValues: () => PorterFormVariableList;
   isReadOnly?: boolean;
   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) => {
   const getInitialValidation = (data: PorterFormData) => {
@@ -376,7 +388,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
   using functions for each input to finalize the variables
   using functions for each input to finalize the variables
   This can take care of things like appending units to strings
   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
     // we start off with a base list of the current variables for fields
     // that don't need any processing on top (for example: checkbox)
     // that don't need any processing on top (for example: checkbox)
     // the assign here is important because that way state.variable isn't mutated
     // 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,
       select: getFinalVariablesForSelect,
     };
     };
 
 
-    const data = props.rawFormData.includeHiddenFields
+    const data = props.includeHiddenFields
+      ? restructureToNewFields(props.rawFormData)
+      : props.rawFormData.includeHiddenFields
       ? restructureToNewFields(props.rawFormData)
       ? restructureToNewFields(props.rawFormData)
       : formData;
       : formData;
 
 
@@ -411,7 +425,12 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
       )
       )
     );
     );
     if (props.doDebug) console.log(Object.assign.apply({}, varList));
     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) {
   if (props.doDebug) {
@@ -434,6 +453,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
           error: isValidated ? null : "Missing required fields",
           error: isValidated ? null : "Missing required fields",
         },
         },
         onSubmit: onSubmitWrapper,
         onSubmit: onSubmitWrapper,
+        getSubmitValues,
       }}
       }}
     >
     >
       {props.children}
       {props.children}

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

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

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

@@ -1,10 +1,6 @@
 import React from "react";
 import React from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-import {
-  ArrayInputField,
-  ArrayInputFieldState,
-  GetFinalVariablesFunction,
-} from "../types";
+import { ArrayInputField, ArrayInputFieldState, GetFinalVariablesFunction } from "../types";
 import useFormField from "../hooks/useFormField";
 import useFormField from "../hooks/useFormField";
 
 
 const ArrayInput: React.FC<ArrayInputField> = (props) => {
 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 React from "react";
-import {
-  ArrayInputField,
-  CheckboxField,
-  CheckboxFieldState,
-  GetFinalVariablesFunction,
-} from "../types";
+import { CheckboxField, CheckboxFieldState, GetFinalVariablesFunction } from "../types";
 import CheckboxRow from "../../form-components/CheckboxRow";
 import CheckboxRow from "../../form-components/CheckboxRow";
 import useFormField from "../hooks/useFormField";
 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 InputRow from "../../form-components/InputRow";
 import useFormField from "../hooks/useFormField";
 import useFormField from "../hooks/useFormField";
 import {
 import {
-  GenericInputField,
   GetFinalVariablesFunction,
   GetFinalVariablesFunction,
   InputField,
   InputField,
   StringInputFieldState,
   StringInputFieldState,
@@ -50,8 +49,6 @@ const Input: React.FC<InputField> = ({
     return <></>;
     return <></>;
   }
   }
 
 
-  console.log(value);
-
   const curValue =
   const curValue =
     settings?.type == "number"
     settings?.type == "number"
       ? !isNaN(parseFloat(variables[variable]))
       ? !isNaN(parseFloat(variables[variable]))

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

@@ -1,10 +1,5 @@
 import React from "react";
 import React from "react";
-import {
-  GetFinalVariablesFunction,
-  InputField,
-  KeyValueArrayField,
-  KeyValueArrayFieldState,
-} from "../types";
+import { GetFinalVariablesFunction, KeyValueArrayField, KeyValueArrayFieldState } from "../types";
 import sliders from "../../../assets/sliders.svg";
 import sliders from "../../../assets/sliders.svg";
 import upload from "../../../assets/upload.svg";
 import upload from "../../../assets/upload.svg";
 import styled from "styled-components";
 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 React, { useContext } from "react";
-import {
-  CheckboxField,
-  GetFinalVariablesFunction,
-  SelectField,
-  SelectFieldState,
-} from "../types";
+import { GetFinalVariablesFunction, SelectField, SelectFieldState } from "../types";
 import Selector from "../../Selector";
 import Selector from "../../Selector";
 import styled from "styled-components";
 import styled from "styled-components";
 import useFormField from "../hooks/useFormField";
 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 React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { hardcodedNames, hardcodedIcons } from "shared/hardcodedNameDict";
+import { hardcodedIcons, hardcodedNames } from "shared/hardcodedNameDict";
 
 
 type PropsType = {
 type PropsType = {
   service: {
   service: {

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

@@ -1,10 +1,6 @@
 import { useContext, useEffect } from "react";
 import { useContext, useEffect } from "react";
 import { PorterFormContext } from "../PorterFormContextProvider";
 import { PorterFormContext } from "../PorterFormContextProvider";
-import {
-  PorterFormFieldFieldState,
-  PorterFormFieldValidationState,
-  PorterFormVariableList,
-} from "../types";
+import { PorterFormFieldFieldState, PorterFormFieldValidationState, PorterFormVariableList } from "../types";
 
 
 interface FormFieldData<T> {
 interface FormFieldData<T> {
   state: 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 styled from "styled-components";
 
 
 import { ActionConfigType } from "shared/types";
 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 React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
@@ -8,7 +7,6 @@ import api from "shared/api";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import { ActionConfigType } from "../../shared/types";
 import { ActionConfigType } from "../../shared/types";
 import InputRow from "../form-components/InputRow";
 import InputRow from "../form-components/InputRow";
-import InfoTooltip from "components/InfoTooltip";
 
 
 type PropsType = {
 type PropsType = {
   actionConfig: ActionConfigType | null;
   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 styled from "styled-components";
 import branch_icon from "assets/branch.png";
 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 api from "../../shared/api";
 import { Context } from "../../shared/Context";
 import { Context } from "../../shared/Context";
-import { FileType, ActionConfigType } from "../../shared/types";
+import { ActionConfigType, FileType } from "../../shared/types";
 
 
 import Loading from "../Loading";
 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 styled from "styled-components";
 import github from "assets/github.png";
 import github from "assets/github.png";
 
 
 import api from "shared/api";
 import api from "shared/api";
-import { RepoType, ActionConfigType } from "shared/types";
+import { ActionConfigType, RepoType } from "shared/types";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
 import Loading from "../Loading";
 import Loading from "../Loading";
 import SearchBar from "../SearchBar";
 import SearchBar from "../SearchBar";
-import Helper from "../form-components/Helper";
 
 
 interface GithubAppAccessData {
 interface GithubAppAccessData {
   has_access: boolean;
   has_access: boolean;

+ 1 - 2
dashboard/src/index.html

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

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

@@ -1,6 +1,5 @@
 import React, { Component } from "react";
 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 api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
@@ -208,44 +207,12 @@ export default class Main extends Component<PropsType, StateType> {
 
 
   render() {
   render() {
     return (
     return (
-      <StyledMain>
-        <GlobalStyle />
+      <>
         {this.renderMain()}
         {this.renderMain()}
         <CurrentError currentError={this.context.currentError} />
         <CurrentError currentError={this.context.currentError} />
-      </StyledMain>
+      </>
     );
     );
   }
   }
 }
 }
 
 
 Main.contextType = Context;
 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 React, { Component } from "react";
-import { BrowserRouter } from "react-router-dom";
 
 
 import { ContextProvider } from "../shared/Context";
 import { ContextProvider } from "../shared/Context";
 import Main from "./Main";
 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 styled from "styled-components";
 import logo from "assets/logo.png";
 import logo from "assets/logo.png";
 import github from "assets/github-icon.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 styled from "styled-components";
 import logo from "assets/logo.png";
 import logo from "assets/logo.png";
 
 
 import api from "shared/api";
 import api from "shared/api";
-import { emailRegex } from "shared/regex";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
 type PropsType = {
 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 api from "shared/api";
 import { H } from "highlight.run";
 import { H } from "highlight.run";
 import { Context } from "shared/Context";
 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 { ClusterType, ProjectType } from "shared/types";
 
 
 import ConfirmOverlay from "components/ConfirmOverlay";
 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 styled from "styled-components";
 import monojob from "assets/monojob.png";
 import monojob from "assets/monojob.png";
 import monoweb from "assets/monoweb.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 { Context } from "shared/Context";
 import { ChartType, ClusterType } from "shared/types";
 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 DashboardHeader from "./DashboardHeader";
 import ChartList from "./chart/ChartList";
 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 { Context } from "shared/Context";
 import api from "shared/api";
 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 { PorterUrl } from "shared/routing";
 
 
 import Chart from "./Chart";
 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 React, { useContext, useEffect, useMemo, useState } from "react";
 
 
 import Table from "components/Table";
 import Table from "components/Table";
-import { Column, Row } from "react-table";
+import { Column } from "react-table";
 import styled from "styled-components";
 import styled from "styled-components";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 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 React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-
-import sliders from "assets/sliders.svg";
 import api from "shared/api";
 import api from "shared/api";
 
 
 import { Context } from "shared/Context";
 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 sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
 import upload from "assets/upload.svg";
-import { keysIn } from "lodash";
 
 
 export type KeyValueType = {
 export type KeyValueType = {
   key: string;
   key: string;

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

@@ -1,19 +1,16 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-import close from "assets/close.png";
 import backArrow from "assets/back_arrow.png";
 import backArrow from "assets/back_arrow.png";
 import key from "assets/key.svg";
 import key from "assets/key.svg";
-import _ from "lodash";
 import loading from "assets/loading.gif";
 import loading from "assets/loading.gif";
 
 
-import { ChartType, StorageType, ClusterType } from "shared/types";
+import { ClusterType } from "shared/types";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import { isAlphanumeric } from "shared/common";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import api from "shared/api";
 
 
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
-import Loading from "components/Loading";
 import TabRegion from "components/TabRegion";
 import TabRegion from "components/TabRegion";
 import EnvGroupArray, { KeyValueType } from "./EnvGroupArray";
 import EnvGroupArray, { KeyValueType } from "./EnvGroupArray";
 import Heading from "components/form-components/Heading";
 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 styled from "styled-components";
 import yaml from "js-yaml";
 import yaml from "js-yaml";
 import backArrow from "assets/back_arrow.png";
 import backArrow from "assets/back_arrow.png";
 import _ from "lodash";
 import _ from "lodash";
 import loadingSrc from "assets/loading.gif";
 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 { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
-
-import Loading from "components/Loading";
 import StatusIndicator from "components/StatusIndicator";
 import StatusIndicator from "components/StatusIndicator";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import RevisionSection from "./RevisionSection";
 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 { Context } from "shared/Context";
 import { RouteComponentProps, withRouter } from "react-router";
 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 api from "shared/api";
-import { PorterUrl, pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import ExpandedJobChart from "./ExpandedJobChart";
 import ExpandedJobChart from "./ExpandedJobChart";
 import ExpandedChart from "./ExpandedChart";
 import ExpandedChart from "./ExpandedChart";
 import Loading from "components/Loading";
 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 _ from "lodash";
 import loading from "assets/loading.gif";
 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 { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
 
 
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
-import Loading from "components/Loading";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
-import JobList from "./jobs/JobList";
+import TempJobList from "./jobs/TempJobList";
 import SettingsSection from "./SettingsSection";
 import SettingsSection from "./SettingsSection";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
-import { PlaceHolder } from "brace";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 
 type PropsType = WithAuthProps & {
 type PropsType = WithAuthProps & {
@@ -419,25 +417,6 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
   };
   };
 
 
   renderTabContents = (currentTab: string, submitValues?: any) => {
   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) {
     switch (currentTab) {
       case "jobs":
       case "jobs":
         if (this.state.imageIsPlaceholder) {
         if (this.state.imageIsPlaceholder) {
@@ -455,12 +434,12 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
         }
         }
         return (
         return (
           <TabWrapper>
           <TabWrapper>
-            {saveButton}
-            <JobList
+            <TempJobList
+              handleSaveValues={this.handleSaveValues}
               jobs={this.state.jobs}
               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>
           </TabWrapper>
         );
         );
@@ -616,14 +595,14 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
                     this.state.imageIsPlaceholder ||
                     this.state.imageIsPlaceholder ||
                     !this.props.isAuthorized("job", "", ["get", "update"])
                     !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}
                   leftTabOptions={this.state.leftTabOptions}
                   rightTabOptions={this.state.rightTabOptions}
                   rightTabOptions={this.state.rightTabOptions}
                   saveValuesStatus={this.state.saveValuesStatus}
                   saveValuesStatus={this.state.saveValuesStatus}
                   saveButtonText="Save Config"
                   saveButtonText="Save Config"
+                  includeHiddenFields
                 />
                 />
               )}
               )}
             </BodyWrapper>
             </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 styled from "styled-components";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { ResourceType, ChartType } from "shared/types";
+import { ChartType, ResourceType } from "shared/types";
 
 
 import GraphDisplay from "./graph/GraphDisplay";
 import GraphDisplay from "./graph/GraphDisplay";
 import Loading from "components/Loading";
 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 yaml from "js-yaml";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { ResourceType, ChartType } from "shared/types";
+import { ChartType, ResourceType } from "shared/types";
 
 
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import ResourceTab from "components/ResourceTab";
 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 styled from "styled-components";
 import api from "shared/api";
 import api from "shared/api";
 import yaml from "js-yaml";
 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 { Context } from "shared/Context";
 
 
 import ImageSelector from "components/image-selector/ImageSelector";
 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 React, { Component } from "react";
 import styled from "styled-components";
 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 Node from "./Node";
 import Edge from "./Edge";
 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 styled from "styled-components";
 import yaml from "js-yaml";
 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";
 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 styled from "styled-components";
 
 
 import api from "shared/api";
 import api from "shared/api";
-import _ from "lodash";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import JobResource from "./JobResource";
 import JobResource from "./JobResource";
 import ConfirmOverlay from "components/ConfirmOverlay";
 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 styled from "styled-components";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import _ from "lodash";
 import _ from "lodash";
@@ -7,7 +7,6 @@ import api from "shared/api";
 import Logs from "../status/Logs";
 import Logs from "../status/Logs";
 import plus from "assets/plus.svg";
 import plus from "assets/plus.svg";
 import closeRounded from "assets/close-rounded.png";
 import closeRounded from "assets/close-rounded.png";
-import trash from "assets/trash.png";
 import KeyValueArray from "components/form-components/KeyValueArray";
 import KeyValueArray from "components/form-components/KeyValueArray";
 
 
 type PropsType = {
 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 { 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 { localPoint } from "@visx/event";
 import { LinearGradient } from "@visx/gradient";
 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 { timeFormat } from "d3-time-format";
 import { NormalizedMetricsData } from "./types";
 import { NormalizedMetricsData } from "./types";
 
 

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

@@ -1,12 +1,15 @@
 import {
 import {
+  AvailableMetrics,
   GenericMetricResponse,
   GenericMetricResponse,
-  NormalizedMetricsData,
-  MetricsMemoryDataResponse,
   MetricsCPUDataResponse,
   MetricsCPUDataResponse,
+  MetricsHpaReplicasDataResponse,
+  MetricsMemoryDataResponse,
   MetricsNetworkDataResponse,
   MetricsNetworkDataResponse,
   MetricsNGINXErrorsDataResponse,
   MetricsNGINXErrorsDataResponse,
   AvailableMetrics,
   AvailableMetrics,
-  MetricsHpaReplicasDataResponse, MetricsNGINXLatencyDataResponse
+  MetricsHpaReplicasDataResponse, 
+  MetricsNGINXLatencyDataResponse
+  NormalizedMetricsData,
 } from "./types";
 } 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 styled from "styled-components";
 
 
 import api from "shared/api";
 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 { Context } from "shared/Context";
 import api from "shared/api";
 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 Helper from "components/form-components/Helper";
 import { pushFiltered } from "shared/routing";
 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 FormDebugger from "components/porter-form/FormDebugger";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 
 
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered, pushQueryParams } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 
 type PropsType = RouteComponentProps &
 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 styled from "styled-components";
-import GHIcon from "assets/GithubIcon";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import { integrationList } from "shared/common";
 import { integrationList } from "shared/common";
@@ -9,7 +8,6 @@ import IntegrationList from "./IntegrationList";
 import api from "shared/api";
 import api from "shared/api";
 import { pushFiltered } from "shared/routing";
 import { pushFiltered } from "shared/routing";
 import Loading from "../../../components/Loading";
 import Loading from "../../../components/Loading";
-import ConfirmOverlay from "../../../components/ConfirmOverlay";
 import SlackIntegrationList from "./SlackIntegrationList";
 import SlackIntegrationList from "./SlackIntegrationList";
 import TitleSection from "components/TitleSection";
 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 { Route, RouteComponentProps, Switch, withRouter } from "react-router";
 
 
 import { integrationList } from "shared/common";
 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 ConfirmOverlay from "../../../components/ConfirmOverlay";
 import styled from "styled-components";
 import styled from "styled-components";
 import { Context } from "../../../shared/Context";
 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 SettingsPage from "./SettingsPage";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 
 
-import {
-  PorterTemplate,
-  ActionConfigType,
-  ChoiceType,
-  ClusterType,
-  StorageType,
-} from "shared/types";
+import { ActionConfigType, PorterTemplate, StorageType } from "shared/types";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
   currentTab?: string;
   currentTab?: string;
@@ -79,7 +73,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
     selectedRegistry: null as any,
     selectedRegistry: null as any,
   };
   };
 
 
-  createGHAction = (chartName: string, chartNamespace: string, env?: any) => {
+  createGHAction = (chartName: string, chartNamespace: string) => {
     let { currentProject, currentCluster, setCurrentError } = this.context;
     let { currentProject, currentCluster, setCurrentError } = this.context;
     let {
     let {
       actionConfig,
       actionConfig,
@@ -106,7 +100,6 @@ class LaunchFlow extends Component<PropsType, StateType> {
           folder_path: folderPath,
           folder_path: folderPath,
           image_repo_uri: imageRepoUri,
           image_repo_uri: imageRepoUri,
           git_repo_id: actionConfig.git_repo_id,
           git_repo_id: actionConfig.git_repo_id,
-          env: env,
         },
         },
         {
         {
           project_id: currentProject.id,
           project_id: currentProject.id,
@@ -320,8 +313,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
       )
       )
       .then((res: any) => {
       .then((res: any) => {
         if (sourceType === "repo") {
         if (sourceType === "repo") {
-          let env = rawValues["container.env.normal"];
-          this.createGHAction(name, selectedNamespace, env);
+          this.createGHAction(name, selectedNamespace);
         }
         }
         // this.props.setCurrentView('cluster-dashboard');
         // this.props.setCurrentView('cluster-dashboard');
         this.setState({ saveValuesStatus: "successful" }, () => {
         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 { Context } from "shared/Context";
 
 
-import {
-  ActionConfigType,
-  ChoiceType,
-  ClusterType,
-  StorageType,
-} from "shared/types";
+import { ChoiceType, ClusterType } from "shared/types";
 
 
 import { isAlphanumeric } from "shared/common";
 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 styled from "styled-components";
 import close from "assets/close.png";
 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 styled from "styled-components";
 import close from "assets/close.png";
 import close from "assets/close.png";
 import AceEditor from "react-ace";
 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 Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import { KeyValue } from "components/form-components/KeyValueArray";
 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 = {
 type PropsType = {
   namespace: string;
   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 styled from "styled-components";
 import close from "assets/close.png";
 import close from "assets/close.png";
 import api from "shared/api";
 import api from "shared/api";

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

@@ -1,13 +1,10 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-
-import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
 import Feedback from "./Feedback";
 import Feedback from "./Feedback";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 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 & {
 type PropsType = WithAuthProps & {
   logOut: () => void;
   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 styled from "styled-components";
 
 
 import { InviteType } from "shared/types";
 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 { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 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 SelectRow from "components/form-components/SelectRow";
 import InputRow from "components/form-components/InputRow";
 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 { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 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 InputRow from "components/form-components/InputRow";
 import CheckboxRow from "components/form-components/CheckboxRow";
 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 { ProjectType } from "shared/types";
 import { isAlphanumeric } from "shared/common";
 import { isAlphanumeric } from "shared/common";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 
 
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import { RouteComponentProps, withRouter } from "react-router";
 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 { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 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 UploadArea from "components/form-components/UploadArea";
 import SelectRow from "components/form-components/SelectRow";
 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 InfraStatuses from "./InfraStatuses";
 import ProvisionerLogs from "./ProvisionerLogs";
 import ProvisionerLogs from "./ProvisionerLogs";
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
-import { stringify } from "qs";
-import { forEach } from "lodash";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
   setRefreshClusters: (x: boolean) => void;
   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 ansiparse from "shared/ansiparser";
 import loading from "assets/loading.gif";
 import loading from "assets/loading.gif";
-import warning from "assets/warning.png";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
   selectedInfra: InfraType;
   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 { Context } from "shared/Context";
 import { ProjectType } from "shared/types";
 import { ProjectType } from "shared/types";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {

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

@@ -1,5 +1,4 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
-import styled from "styled-components";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import ProjectSection from "./ProjectSection";
 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 ClusterSection from "./ClusterSection";
 import ProjectSectionContainer from "./ProjectSectionContainer";
 import ProjectSectionContainer from "./ProjectSectionContainer";
-import loading from "assets/loading.gif";
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
-import { pushFiltered, pushQueryParams } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 
 type PropsType = RouteComponentProps &
 type PropsType = RouteComponentProps &

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

@@ -1,11 +1,6 @@
 import React, { Component } from "react";
 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";
 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;
     dockerfile_path: string;
     folder_path: string;
     folder_path: string;
     git_repo_id: number;
     git_repo_id: number;
-    env: any;
   },
   },
   {
   {
     project_id: number;
     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 { AuthContext } from "./AuthContext";
 import { isAuthorized } from "./authorization-helpers";
 import { isAuthorized } from "./authorization-helpers";
 import { ScopeType, Verbs } from "./types";
 import { ScopeType, Verbs } from "./types";

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

@@ -1,6 +1,6 @@
 import UnauthorizedPage from "components/UnauthorizedPage";
 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 { AuthContext } from "./AuthContext";
 import { isAuthorized } from "./authorization-helpers";
 import { isAuthorized } from "./authorization-helpers";
 import { ScopeType, Verbs } from "./types";
 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 digitalOcean from "../assets/do.png";
 import gcp from "../assets/gcp.png";
 import gcp from "../assets/gcp.png";
 import github from "../assets/github.png";
 import github from "../assets/github.png";
-import { InfraType } from "../shared/types";
 
 
 export const infraNames: any = {
 export const infraNames: any = {
   ecr: "Elastic Container Registry (ECR)",
   ecr: "Elastic Container Registry (ECR)",

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

@@ -1,5 +1,3 @@
-import { Location } from "history";
-
 export type PorterUrl =
 export type PorterUrl =
   | "dashboard"
   | "dashboard"
   | "launch"
   | "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)
 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.
 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
 // CreateGitAction represents the accepted values for creating a
 // github action integration
 // github action integration
 type CreateGitAction struct {
 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
 // 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{
 	return &models.GitActionConfig{
 		ReleaseID:            ca.ReleaseID,
 		ReleaseID:            ca.ReleaseID,
 		GitRepo:              ca.GitRepo,
 		GitRepo:              ca.GitRepo,
@@ -29,6 +28,7 @@ func (ca *CreateGitAction) ToGitActionConfig() (*models.GitActionConfig, error)
 		FolderPath:           ca.FolderPath,
 		FolderPath:           ca.FolderPath,
 		GithubInstallationID: ca.GitRepoID,
 		GithubInstallationID: ca.GitRepoID,
 		IsInstallation:       true,
 		IsInstallation:       true,
+		Version:              version,
 	}, nil
 	}, 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
 	GithubConf           *oauth2.Config // one of these will let us authenticate
 	GithubAppID          int64
 	GithubAppID          int64
+	GithubAppSecretPath  string
 	GithubInstallationID uint
 	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
 	GitBranch      string
 	DockerFilePath string
 	DockerFilePath string
@@ -44,6 +44,7 @@ type GithubActions struct {
 	ImageRepoURL   string
 	ImageRepoURL   string
 
 
 	defaultBranch string
 	defaultBranch string
+	Version       string
 }
 }
 
 
 func (g *GithubActions) Setup() (string, error) {
 func (g *GithubActions) Setup() (string, error) {
@@ -66,24 +67,8 @@ func (g *GithubActions) Setup() (string, error) {
 
 
 	g.defaultBranch = repo.GetDefaultBranch()
 	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
 		return "", err
 	}
 	}
 
 
@@ -139,6 +124,7 @@ type GithubActionYAMLStep struct {
 	Timeout uint64            `yaml:"timeout-minutes,omitempty"`
 	Timeout uint64            `yaml:"timeout-minutes,omitempty"`
 	Uses    string            `yaml:"uses,omitempty"`
 	Uses    string            `yaml:"uses,omitempty"`
 	Run     string            `yaml:"run,omitempty"`
 	Run     string            `yaml:"run,omitempty"`
+	With    map[string]string `yaml:"with,omitempty"`
 	Env     map[string]string `yaml:"env,omitempty"`
 	Env     map[string]string `yaml:"env,omitempty"`
 }
 }
 
 
@@ -166,8 +152,7 @@ type GithubActionYAML struct {
 func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 	gaSteps := []GithubActionYAMLStep{
 	gaSteps := []GithubActionYAMLStep{
 		getCheckoutCodeStep(),
 		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
 	branch := g.GitBranch
@@ -229,7 +214,7 @@ func (g *GithubActions) getClient() (*github.Client, error) {
 		http.DefaultTransport,
 		http.DefaultTransport,
 		g.GithubAppID,
 		g.GithubAppID,
 		int64(g.GithubInstallationID),
 		int64(g.GithubInstallationID),
-		"/porter/docker/github_app_private_key.pem")
+		g.GithubAppSecretPath)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err

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

@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"fmt"
 )
 )
 
 
+const updateAppActionName = "porter-dev/porter-update-action"
+
 func getCheckoutCodeStep() GithubActionYAMLStep {
 func getCheckoutCodeStep() GithubActionYAMLStep {
 	return GithubActionYAMLStep{
 	return GithubActionYAMLStep{
 		Name: "Checkout code",
 		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{
 	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 (
 import (
 	"bytes"
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"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 {
 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{
 	client := &http.Client{
 		Timeout: time.Second * 5,
 		Timeout: time.Second * 5,
 	}
 	}
 
 
 	for _, slackInt := range s.slackInts {
 	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
 	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
 		// add this as a bearer token
-		authInfoMap[authInfoName].Token = tok
+		authInfoMap[authInfoName].Token = tok.AccessToken
 	case models.AWS:
 	case models.AWS:
 		awsAuth, err := conf.Repo.AWSIntegration.ReadAWSIntegration(
 		awsAuth, err := conf.Repo.AWSIntegration.ReadAWSIntegration(
 			cluster.AWSIntegrationID,
 			cluster.AWSIntegrationID,

+ 2 - 0
internal/models/gitrepo.go

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

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

@@ -4,6 +4,7 @@ import (
 	"context"
 	"context"
 	"encoding/json"
 	"encoding/json"
 
 
+	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
 	"golang.org/x/oauth2/google"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 )
 )
@@ -83,13 +84,16 @@ func (g *GCPIntegration) GetBearerToken(
 	getTokenCache GetTokenCacheFunc,
 	getTokenCache GetTokenCacheFunc,
 	setTokenCache SetTokenCacheFunc,
 	setTokenCache SetTokenCacheFunc,
 	scopes ...string,
 	scopes ...string,
-) (string, error) {
+) (*oauth2.Token, error) {
 	cache, err := getTokenCache()
 	cache, err := getTokenCache()
 
 
 	// check the token cache for a non-expired token
 	// check the token cache for a non-expired token
 	if cache != nil {
 	if cache != nil {
 		if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 {
 		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 {
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 	}
 
 
 	tok, err := creds.TokenSource.Token()
 	tok, err := creds.TokenSource.Token()
 
 
 	if err != nil {
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 	}
 
 
 	// update the token cache
 	// update the token cache
 	setTokenCache(tok.AccessToken, tok.Expiry)
 	setTokenCache(tok.AccessToken, tok.Expiry)
 
 
-	return tok.AccessToken, nil
+	return tok, nil
 }
 }
 
 
 // credentialsFile is the unmarshalled representation of a GCP credentials file.
 // 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) {
 func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, error) {
+	getTokenCache := r.getTokenCacheFunc(repo)
+
 	gcp, err := repo.GCPIntegration.ReadGCPIntegration(
 	gcp, err := repo.GCPIntegration.ReadGCPIntegration(
 		r.GCPIntegrationID,
 		r.GCPIntegrationID,
 	)
 	)
@@ -102,7 +104,7 @@ func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, er
 
 
 	// get oauth2 access token
 	// get oauth2 access token
 	_, err = gcp.GetBearerToken(
 	_, err = gcp.GetBearerToken(
-		r.getTokenCache,
+		getTokenCache,
 		r.setTokenCacheFunc(repo),
 		r.setTokenCacheFunc(repo),
 		"https://www.googleapis.com/auth/devstorage.read_write",
 		"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
 	// it's now written to the token cache, so return
-	cache, err := r.getTokenCache()
+	cache, err := getTokenCache()
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -352,11 +354,18 @@ func (r *Registry) listPrivateRegistryRepositories(
 	return res, nil
 	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(
 func (r *Registry) setTokenCacheFunc(

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików