소스 검색

Merge branch 'master' of github.com:porter-dev/porter into fix/logs-overflow

Soham Parekh 3 년 전
부모
커밋
bf40ae8068
68개의 변경된 파일1236개의 추가작업 그리고 966개의 파일을 삭제
  1. 2 2
      .github/workflows/prerelease.yaml
  2. 1 1
      api/client/release.go
  3. 1 1
      api/server/authz/release.go
  4. 2 0
      api/server/handlers/environment/list_deployments_by_cluster.go
  5. 4 0
      api/server/handlers/infra/forms.go
  6. 1 1
      api/server/handlers/namespace/create_env_group.go
  7. 1 1
      api/server/handlers/release/create.go
  8. 1 1
      api/server/handlers/release/create_addon.go
  9. 1 1
      api/server/handlers/release/create_webhook.go
  10. 1 1
      api/server/handlers/release/delete.go
  11. 1 1
      api/server/handlers/release/get.go
  12. 1 1
      api/server/handlers/release/get_all_pods.go
  13. 1 1
      api/server/handlers/release/get_components.go
  14. 1 1
      api/server/handlers/release/get_controllers.go
  15. 1 1
      api/server/handlers/release/get_job_status.go
  16. 1 1
      api/server/handlers/release/get_jobs.go
  17. 1 1
      api/server/handlers/release/get_latest_job_run.go
  18. 1 1
      api/server/handlers/release/stream_form.go
  19. 1 1
      api/server/handlers/release/update_git_action_config.go
  20. 1 1
      api/server/handlers/release/update_rollback.go
  21. 1 1
      api/server/handlers/release/upgrade.go
  22. 1 1
      api/server/handlers/stack/add_application.go
  23. 1 1
      api/server/handlers/stack/create.go
  24. 1 1
      api/server/handlers/stack/helpers.go
  25. 1 1
      api/server/handlers/v1/env_group/create.go
  26. 1 1
      api/server/handlers/v1/release/upgrade.go
  27. 8 6
      api/types/environment.go
  28. 1 1
      api/types/namespace.go
  29. 1 1
      api/types/release.go
  30. 1 1
      api/types/template.go
  31. 1 1
      cli/cmd/get.go
  32. 1 1
      cli/cmd/list.go
  33. 1 1
      dashboard/src/components/form-components/Helper.tsx
  34. 12 11
      dashboard/src/main/home/Home.tsx
  35. 15 8
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx
  36. 352 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx
  37. 22 368
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx
  38. 535 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx
  39. 2 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/types.ts
  40. 5 4
      dashboard/src/main/home/project-settings/ProjectSettings.tsx
  41. 9 7
      dashboard/src/main/home/sidebar/Sidebar.tsx
  42. 3 1
      dashboard/src/shared/Context.tsx
  43. 81 89
      go.mod
  44. 69 284
      go.sum
  45. 1 1
      internal/adapter/gorm.go
  46. 4 4
      internal/helm/agent.go
  47. 3 3
      internal/helm/agent_test.go
  48. 5 5
      internal/helm/config.go
  49. 2 2
      internal/helm/loader/loader.go
  50. 1 1
      internal/helm/postrenderer.go
  51. 1 1
      internal/helm/repo/repo.go
  52. 2 2
      internal/helm/storage.go
  53. 3 3
      internal/helm/storage_test.go
  54. 1 1
      internal/kubernetes/agent.go
  55. 1 1
      internal/kubernetes/envgroup/create.go
  56. 1 1
      internal/opa/opa.go
  57. 1 1
      internal/repository/gorm/api_token.go
  58. 31 0
      internal/repository/gorm/api_token_test.go
  59. 20 0
      internal/repository/gorm/helpers_test.go
  60. 1 1
      internal/stacks/hooks.go
  61. 1 1
      internal/templater/helm/manifests/reader.go
  62. 2 2
      internal/templater/helm/values/reader.go
  63. 1 1
      internal/templater/helm/values/writer.go
  64. 2 2
      internal/templater/parser/parser.go
  65. 0 13
      provisioner/integrations/storage/s3/s3.go
  66. 0 109
      provisioner/server/handlers/state/create_resource.go
  67. 1 1
      workers/jobs/helm_revisions_count_tracker.go
  68. 1 1
      workers/utils/retry_helm_agent.go

+ 2 - 2
.github/workflows/prerelease.yaml

@@ -149,7 +149,7 @@ jobs:
         env:
           GOOS: linux
           GOARCH: amd64
-          CGO_ENABLED: 1
+          CGO_ENABLED: 0
       # Note: we have to zip all binaries before uploading them as artifacts --
       # without this step, the binaries will be uploaded but the file metadata will
       # be listed as plaintext after downloading the artifact in a later step
@@ -211,7 +211,7 @@ jobs:
         env:
           GOOS: darwin
           GOARCH: amd64
-          CGO_ENABLED: 1
+          CGO_ENABLED: 0
       - name: Upload binaries
         uses: actions/upload-artifact@v2
         with:

+ 1 - 1
api/client/release.go

@@ -5,7 +5,7 @@ import (
 	"fmt"
 
 	"github.com/porter-dev/porter/api/types"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 func (c *Client) ListReleases(

+ 1 - 1
api/server/authz/release.go

@@ -11,7 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type ReleaseScopedFactory struct {

+ 2 - 0
api/server/handlers/environment/list_deployments_by_cluster.go

@@ -293,6 +293,8 @@ func fetchOpenPullRequests(
 				RepoName:   env.GitRepoName,
 				BranchFrom: pr.GetHead().GetRef(),
 				BranchInto: pr.GetBase().GetRef(),
+				CreatedAt:  pr.GetCreatedAt(),
+				UpdatedAt:  pr.GetUpdatedAt(),
 			})
 		}
 	}

+ 4 - 0
api/server/handlers/infra/forms.go

@@ -444,6 +444,10 @@ tabs:
           value: "1.21"
         - label: "1.22"
           value: "1.22"
+        - label: "1.23"
+          value: "1.23"
+        - label: "1.24"
+          value: "1.24"
     - type: number-input
       label: Minimum number of EC2 instances to create in the application autoscaling group.
       variable: min_instances

+ 1 - 1
api/server/handlers/namespace/create_env_group.go

@@ -8,7 +8,7 @@ import (
 
 	"sigs.k8s.io/yaml"
 
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 	v1 "k8s.io/api/core/v1"
 
 	"github.com/porter-dev/porter/api/server/authz"

+ 1 - 1
api/server/handlers/release/create.go

@@ -25,9 +25,9 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/registry"
+	"github.com/stefanmcshane/helm/pkg/release"
 	"golang.org/x/crypto/bcrypt"
 	"gopkg.in/yaml.v2"
-	"helm.sh/helm/v3/pkg/release"
 	v1 "k8s.io/api/core/v1"
 )
 

+ 1 - 1
api/server/handlers/release/create_addon.go

@@ -15,7 +15,7 @@ import (
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
-	"helm.sh/helm/v3/pkg/chart"
+	"github.com/stefanmcshane/helm/pkg/chart"
 )
 
 type CreateAddonHandler struct {

+ 1 - 1
api/server/handlers/release/create_webhook.go

@@ -9,7 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type CreateWebhookHandler struct {

+ 1 - 1
api/server/handlers/release/delete.go

@@ -13,7 +13,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/integrations/ci/gitlab"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type DeleteReleaseHandler struct {

+ 1 - 1
api/server/handlers/release/get.go

@@ -13,8 +13,8 @@ import (
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/templater/parser"
+	"github.com/stefanmcshane/helm/pkg/release"
 	"gorm.io/gorm"
-	"helm.sh/helm/v3/pkg/release"
 )
 
 type ReleaseGetHandler struct {

+ 1 - 1
api/server/handlers/release/get_all_pods.go

@@ -14,7 +14,7 @@ import (
 	"github.com/porter-dev/porter/internal/helm/grapher"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 	v1 "k8s.io/api/core/v1"
 )
 

+ 1 - 1
api/server/handlers/release/get_components.go

@@ -9,7 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm/grapher"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type GetComponentsHandler struct {

+ 1 - 1
api/server/handlers/release/get_controllers.go

@@ -15,7 +15,7 @@ import (
 	"github.com/porter-dev/porter/internal/helm/grapher"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )

+ 1 - 1
api/server/handlers/release/get_job_status.go

@@ -10,7 +10,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type GetJobsStatusHandler struct {

+ 1 - 1
api/server/handlers/release/get_jobs.go

@@ -12,7 +12,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type GetJobsHandler struct {

+ 1 - 1
api/server/handlers/release/get_latest_job_run.go

@@ -10,7 +10,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type GetLatestJobRunHandler struct {

+ 1 - 1
api/server/handlers/release/stream_form.go

@@ -14,7 +14,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/templater/parser"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type StreamFormHandler struct {

+ 1 - 1
api/server/handlers/release/update_git_action_config.go

@@ -9,8 +9,8 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/stefanmcshane/helm/pkg/release"
 	"gorm.io/gorm"
-	"helm.sh/helm/v3/pkg/release"
 )
 
 type UpdateGitActionConfigHandler struct {

+ 1 - 1
api/server/handlers/release/update_rollback.go

@@ -12,7 +12,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type RollbackReleaseHandler struct {

+ 1 - 1
api/server/handlers/release/upgrade.go

@@ -18,7 +18,7 @@ import (
 	"github.com/porter-dev/porter/internal/notifier"
 	"github.com/porter-dev/porter/internal/notifier/slack"
 	"github.com/porter-dev/porter/internal/stacks"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 var (

+ 1 - 1
api/server/handlers/stack/add_application.go

@@ -14,7 +14,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/stacks"
-	helmrelease "helm.sh/helm/v3/pkg/release"
+	helmrelease "github.com/stefanmcshane/helm/pkg/release"
 )
 
 type StackAddApplicationHandler struct {

+ 1 - 1
api/server/handlers/stack/create.go

@@ -16,7 +16,7 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
 
-	helmrelease "helm.sh/helm/v3/pkg/release"
+	helmrelease "github.com/stefanmcshane/helm/pkg/release"
 )
 
 type StackCreateHandler struct {

+ 1 - 1
api/server/handlers/stack/helpers.go

@@ -6,7 +6,7 @@ import (
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type applyAppResourceOpts struct {

+ 1 - 1
api/server/handlers/v1/env_group/create.go

@@ -8,7 +8,7 @@ import (
 
 	"sigs.k8s.io/yaml"
 
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 	v1 "k8s.io/api/core/v1"
 
 	"github.com/porter-dev/porter/api/server/authz"

+ 1 - 1
api/server/handlers/v1/release/upgrade.go

@@ -18,7 +18,7 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/notifier"
 	"github.com/porter-dev/porter/internal/notifier/slack"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 var (

+ 8 - 6
api/types/environment.go

@@ -129,12 +129,14 @@ type GetDeploymentRequest struct {
 }
 
 type PullRequest struct {
-	Title      string `json:"pr_title"`
-	Number     uint   `json:"pr_number"`
-	RepoOwner  string `json:"repo_owner"`
-	RepoName   string `json:"repo_name"`
-	BranchFrom string `json:"branch_from"`
-	BranchInto string `json:"branch_into"`
+	Title      string    `json:"pr_title"`
+	Number     uint      `json:"pr_number"`
+	RepoOwner  string    `json:"repo_owner"`
+	RepoName   string    `json:"repo_name"`
+	BranchFrom string    `json:"branch_from"`
+	BranchInto string    `json:"branch_into"`
+	CreatedAt  time.Time `json:"created_at"`
+	UpdatedAt  time.Time `json:"updated_at"`
 }
 
 type ToggleNewCommentRequest struct {

+ 1 - 1
api/types/namespace.go

@@ -3,7 +3,7 @@ package types
 import (
 	"time"
 
-	"helm.sh/helm/v3/pkg/action"
+	"github.com/stefanmcshane/helm/pkg/action"
 	v1 "k8s.io/api/core/v1"
 )
 

+ 1 - 1
api/types/release.go

@@ -1,7 +1,7 @@
 package types
 
 import (
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )

+ 1 - 1
api/types/template.go

@@ -2,7 +2,7 @@ package types
 
 import (
 	"github.com/porter-dev/porter/internal/helm/upgrade"
-	"helm.sh/helm/v3/pkg/chart"
+	"github.com/stefanmcshane/helm/pkg/chart"
 )
 
 const (

+ 1 - 1
cli/cmd/get.go

@@ -9,8 +9,8 @@ import (
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/spf13/cobra"
+	"github.com/stefanmcshane/helm/pkg/time"
 	"gopkg.in/yaml.v2"
-	"helm.sh/helm/v3/pkg/time"
 )
 
 // getCmd represents the "porter get" base command when called

+ 1 - 1
cli/cmd/list.go

@@ -10,7 +10,7 @@ import (
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/spf13/cobra"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 var allNamespaces bool

+ 1 - 1
dashboard/src/components/form-components/Helper.tsx

@@ -5,7 +5,7 @@ export const Helper = styled.div<{ color?: string }>`
   color: ${({ color }) => (color ? color : "#aaaabb")};
   line-height: 1.6em;
   font-size: 13px;
-  margin-bottom: 15px;
+  margin-bottom: 35px;
   margin-top: 20px;
 `;
 

+ 12 - 11
dashboard/src/main/home/Home.tsx

@@ -12,7 +12,6 @@ import ConfirmOverlay from "components/ConfirmOverlay";
 import Loading from "components/Loading";
 import ClusterDashboard from "./cluster-dashboard/ClusterDashboard";
 import Dashboard from "./dashboard/Dashboard";
-import WelcomeForm from "./WelcomeForm";
 import Integrations from "./integrations/Integrations";
 import LaunchWrapper from "./launch/LaunchWrapper";
 
@@ -435,16 +434,18 @@ class Home extends Component<PropsType, StateType> {
                 return <Onboarding />;
               }}
             />
-            <Route
-              path="/infrastructure"
-              render={() => {
-                return (
-                  <DashboardWrapper>
-                    <InfrastructureRouter />
-                  </DashboardWrapper>
-                );
-              }}
-            />
+            {this.context.user.isPorterUser ? (
+              <Route
+                path="/infrastructure"
+                render={() => {
+                  return (
+                    <DashboardWrapper>
+                      <InfrastructureRouter />
+                    </DashboardWrapper>
+                  );
+                }}
+              />
+            ) : null}
             <Route
               path="/dashboard"
               render={() => {

+ 15 - 8
dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx

@@ -7,11 +7,13 @@ const BranchFilterSelector = ({
   options,
   onChange,
   showLoading,
+  multiSelect = true,
 }: {
   value: string[];
   options: string[];
   onChange: (value: string[]) => void;
   showLoading?: boolean;
+  multiSelect?: boolean;
 }) => {
   const filteredBranches = useMemo(() => {
     if (!options.length) {
@@ -26,6 +28,11 @@ const BranchFilterSelector = ({
   }, [options, value]);
 
   const handleAddBranch = (branch: string) => {
+    if (!multiSelect) {
+      onChange([branch]);
+      return;
+    }
+
     const newSelectedBranches = [...value, branch];
 
     onChange(newSelectedBranches);
@@ -55,14 +62,14 @@ const BranchFilterSelector = ({
       {/* List selected branches  */}
 
       <BranchRowList>
-      {value.map((branch) => (
-        <BranchRow key={branch}>
-          <div>{branch}</div>
-          <RemoveBranchButton onClick={() => handleDeleteBranch(branch)}>
-            x
-          </RemoveBranchButton>
-        </BranchRow>
-      ))}
+        {value.map((branch) => (
+          <BranchRow key={branch}>
+            <div>{branch}</div>
+            <RemoveBranchButton onClick={() => handleDeleteBranch(branch)}>
+              x
+            </RemoveBranchButton>
+          </BranchRow>
+        ))}
       </BranchRowList>
     </>
   );

+ 352 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx

@@ -0,0 +1,352 @@
+import React, { useContext, useState } from "react";
+import styled from "styled-components";
+import { Context } from "shared/Context";
+import { Environment } from "../types";
+import Helper from "components/form-components/Helper";
+import api from "shared/api";
+import { useQuery } from "@tanstack/react-query";
+import { validatePorterYAML } from "../utils";
+import Banner from "components/Banner";
+import { useRouting } from "shared/routing";
+import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal";
+import Placeholder from "components/Placeholder";
+import BranchFilterSelector from "../components/BranchFilterSelector";
+import _ from "lodash";
+
+interface Props {
+  environmentID: string;
+}
+
+const CreateBranchEnvironment = ({ environmentID }: Props) => {
+  const router = useRouting();
+  const [showErrorsModal, setShowErrorsModal] = useState<boolean>(false);
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+
+  const { data: environment } = useQuery<Environment>(
+    ["environment", currentProject.id, currentCluster.id, environmentID],
+    async () => {
+      const { data: environment } = await api.getEnvironment<Environment>(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          environment_id: parseInt(environmentID),
+        }
+      );
+
+      return environment;
+    }
+  );
+
+  // Get all branches for the current environment
+  const { isLoading: branchesLoading, data: branches } = useQuery<string[]>(
+    ["branches", currentProject.id, currentCluster.id, environment],
+    async () => {
+      try {
+        const res = await api.getBranches<string[]>(
+          "<token>",
+          {},
+          {
+            project_id: currentProject.id,
+            kind: "github",
+            name: environment.git_repo_name,
+            owner: environment.git_repo_owner,
+            git_repo_id: environment.git_installation_id,
+          }
+        );
+        return res.data ?? [];
+      } catch (err) {
+        setCurrentError(
+          "Couldn't load branches for this repository, please try again later."
+        );
+      }
+    },
+    {
+      enabled: !!environment,
+    }
+  );
+
+  const [selectedBranch, setSelectedBranch] = useState<string>();
+  const [loading, setLoading] = useState(false);
+  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
+
+  const handleRowItemClick = async (branch: string) => {
+    setSelectedBranch(branch);
+    setLoading(true);
+
+    const res = await validatePorterYAML({
+      projectID: currentProject.id,
+      clusterID: currentCluster.id,
+      environmentID: Number(environmentID),
+      branch,
+    });
+
+    setPorterYAMLErrors(res.data.errors ?? []);
+
+    setLoading(false);
+  };
+
+  const handleCreatePreviewDeployment = async () => {
+    try {
+      //   await api.createPreviewEnvironmentDeployment(
+      //     "<token>",
+      //     {
+      //       pr_title: "",
+      //       pr_number: 0,
+      //       repo_owner: environment.git_repo_name,
+      //       repo_name: environment.git_repo_owner,
+      //       branch_from: selectedBranch,
+      //       branch_into: selectedBranch,
+      //     },
+      //     {
+      //       cluster_id: currentCluster?.id,
+      //       project_id: currentProject?.id,
+      //     }
+      //   );
+
+      throw Error("Not implemented yet. (CreateBranchEnvironment.tsx:");
+
+      router.push(
+        `/preview-environments/deployments/${environmentID}/${environment.git_repo_name}/${environment.git_repo_owner}?status_filter=all`
+      );
+    } catch (err) {
+      setCurrentError(err);
+    }
+  };
+
+  if (!branches?.length) {
+    return (
+      <>
+        <Br height="30px" />
+        <Placeholder height="370px">You do not have any branches.</Placeholder>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <Helper>
+        Select a branch to preview. Branches must contain a{" "}
+        <Code>porter.yaml</Code> file.
+      </Helper>
+      <Br height="10px" />
+      <BranchFilterSelector
+        onChange={(branches) => setSelectedBranch(branches[0])}
+        options={branches}
+        value={_.compact([selectedBranch])}
+        showLoading={branchesLoading}
+        multiSelect={false}
+      />
+      {showErrorsModal && selectedBranch ? (
+        <PorterYAMLErrorsModal
+          errors={porterYAMLErrors}
+          onClose={() => setShowErrorsModal(false)}
+          repo={environment.git_repo_name + "/" + environment.git_repo_owner}
+          branch={selectedBranch}
+        />
+      ) : null}
+      {selectedBranch && porterYAMLErrors.length ? (
+        <ValidationErrorBannerWrapper>
+          <Banner type="warning">
+            We found some errors in the porter.yaml file in the&nbsp;
+            {selectedBranch}&nbsp;branch. &nbsp;
+            <LearnMoreButton onClick={() => setShowErrorsModal(true)}>
+              Learn more
+            </LearnMoreButton>
+          </Banner>
+        </ValidationErrorBannerWrapper>
+      ) : null}
+      <CreatePreviewDeploymentWrapper>
+        <SubmitButton
+          onClick={handleCreatePreviewDeployment}
+          disabled={loading || !selectedBranch || porterYAMLErrors.length > 0}
+        >
+          Create preview deployment
+        </SubmitButton>
+        {selectedBranch && porterYAMLErrors.length ? (
+          <RevalidatePorterYAMLSpanWrapper>
+            Please fix your porter.yaml file to continue.{" "}
+            <RevalidateSpan
+              onClick={(e) => {
+                e.preventDefault();
+                e.stopPropagation();
+
+                if (!selectedBranch) {
+                  return;
+                }
+
+                handleRowItemClick(selectedBranch);
+              }}
+            >
+              Refresh
+            </RevalidateSpan>
+          </RevalidatePorterYAMLSpanWrapper>
+        ) : null}
+      </CreatePreviewDeploymentWrapper>
+    </>
+  );
+};
+
+export default CreateBranchEnvironment;
+
+const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
+  width: 100%;
+  padding: 15px;
+  cursor: pointer;
+  background: ${(props) => (props.isSelected ? "#ffffff11" : "#26292e")};
+  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #494b4f")};
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
+const Code = styled.span`
+  font-family: monospace; ;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const DeploymentImageContainer = styled.div`
+  height: 20px;
+  font-size: 13px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  font-weight: 400;
+  justify-content: center;
+  color: #ffffff66;
+  padding-left: 10px;
+`;
+
+const LastDeployed = styled.div`
+  font-size: 13px;
+  margin-top: -1px;
+  margin-left: 10px;
+  display: flex;
+  align-items: center;
+  color: #aaaabb66;
+`;
+
+const MergeInfoWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-right: 8px;
+  position: relative;
+  margin-left: 10px;
+`;
+
+const MergeInfo = styled.div`
+  font-size: 13px;
+  align-items: center;
+  color: #aaaabb66;
+  white-space: nowrap;
+  display: flex;
+  align-items: center;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  max-width: 300px;
+
+  > i {
+    font-size: 16px;
+    margin: 0 2px;
+  }
+`;
+
+const PRIcon = styled.img`
+  font-size: 20px;
+  height: 16px;
+  margin-right: 10px;
+  color: #aaaabb;
+  opacity: 50%;
+`;
+
+const PRName = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  color: #ffffff;
+  display: flex;
+  font-size: 14px;
+  align-items: center;
+  margin-bottom: 10px;
+`;
+
+const SubmitButton = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  font-weight: 500;
+  color: white;
+  height: 30px;
+  padding: 0 8px;
+  width: 200px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
+const Br = styled.div<{ height: string }>`
+  width: 100%;
+  height: ${(props) => props.height || "2px"};
+`;
+
+const ValidationErrorBannerWrapper = styled.div`
+  margin-block: 20px;
+`;
+
+const LearnMoreButton = styled.div`
+  text-decoration: underline;
+  fontweight: bold;
+  cursor: pointer;
+`;
+
+const CreatePreviewDeploymentWrapper = styled.div`
+  margin-top: 30px;
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+`;
+
+const RevalidatePorterYAMLSpanWrapper = styled.div`
+  font-size: 13px;
+  color: #aaaabb;
+`;
+
+const RevalidateSpan = styled.span`
+  color: #aaaabb;
+  text-decoration: underline;
+  cursor: pointer;
+`;

+ 22 - 368
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx

@@ -1,31 +1,21 @@
 import DynamicLink from "components/DynamicLink";
-import Loading from "components/Loading";
-import React, { useContext, useEffect, useState } from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
-import { Context } from "shared/Context";
 import { useParams } from "react-router";
-import { PullRequest } from "../types";
 import DashboardHeader from "../../DashboardHeader";
 import PullRequestIcon from "assets/pull_request_icon.svg";
-import Helper from "components/form-components/Helper";
-import pr_icon from "assets/pull_request_icon.svg";
-import api from "shared/api";
-import { EllipsisTextWrapper, RepoLink } from "../components/styled";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { getPRDeploymentList, validatePorterYAML } from "../utils";
-import Banner from "components/Banner";
-import Modal from "main/home/modals/Modal";
-import { useRouting } from "shared/routing";
-import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal";
-import { PlaceHolder } from "brace";
-import Placeholder from "components/Placeholder";
+import CreatePREnvironment from "./CreatePREnvironment";
+import TabSelector from "components/TabSelector";
+import CreateBranchEnvironment from "./CreateBranchEnvironment";
+
+const TAB_OPTIONS = [
+  { label: "Pull Requests", value: "pull_requests" },
+  { label: "Branches", value: "branches" },
+];
 
 const CreateEnvironment: React.FC = () => {
-  const router = useRouting();
-  const queryClient = useQueryClient();
-  const [showErrorsModal, setShowErrorsModal] = useState<boolean>(false);
-  const { currentProject, currentCluster, setCurrentError } = useContext(
-    Context
+  const [currentTab, setCurrentTab] = useState<typeof TAB_OPTIONS[0]>(
+    TAB_OPTIONS[0]
   );
   const { environment_id, repo_name, repo_owner } = useParams<{
     environment_id: string;
@@ -33,158 +23,8 @@ const CreateEnvironment: React.FC = () => {
     repo_owner: string;
   }>();
 
-  const { isLoading: getPullRequestsLoading, data: pullRequests } = useQuery<
-    PullRequest[]
-  >(
-    ["pullRequests", currentProject.id, currentCluster.id, environment_id],
-    async () => {
-      try {
-        const res = await getPRDeploymentList({
-          projectID: currentProject.id,
-          clusterID: currentCluster.id,
-          environmentID: Number(environment_id),
-        });
-
-        return res.data.pull_requests || [];
-      } catch (err) {
-        setCurrentError(err);
-      }
-    }
-  );
-
-  const [selectedPR, setSelectedPR] = useState<PullRequest>();
-  const [loading, setLoading] = useState(false);
-  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
-
   const selectedRepo = `${repo_owner}/${repo_name}`;
 
-  const handlePRRowItemClick = async (pullRequest: PullRequest) => {
-    setSelectedPR(pullRequest);
-    setLoading(true);
-
-    const res = await validatePorterYAML({
-      projectID: currentProject.id,
-      clusterID: currentCluster.id,
-      environmentID: Number(environment_id),
-      branch: pullRequest.branch_from,
-    });
-
-    setPorterYAMLErrors(res.data.errors ?? []);
-
-    setLoading(false);
-  };
-
-  const handleCreatePreviewDeployment = async () => {
-    try {
-      await api.createPreviewEnvironmentDeployment("<token>", selectedPR, {
-        cluster_id: currentCluster?.id,
-        project_id: currentProject?.id,
-      });
-
-      router.push(
-        `/preview-environments/deployments/${environment_id}/${selectedPR.repo_owner}/${selectedPR.repo_name}?status_filter=all`
-      );
-    } catch (err) {
-      setCurrentError(err);
-    }
-  };
-
-  const renderPullRequestList = () => {
-    return (
-      <>
-        <Helper>
-          Select an open pull request to preview. Pull requests must contain a{" "}
-          <Code>porter.yaml</Code> file.
-        </Helper>
-        <Br height="10px" />
-        <PullRequestList>
-          {(pullRequests ?? []).map((pullRequest: PullRequest, i: number) => {
-            return (
-              <PullRequestRow
-                onClick={() => {
-                  handlePRRowItemClick(pullRequest);
-                }}
-                isLast={i === pullRequests.length - 1}
-                isSelected={pullRequest === selectedPR}
-              >
-                <PRName>
-                  <PRIcon src={pr_icon} alt="pull request icon" />
-                  <EllipsisTextWrapper tooltipText={pullRequest.pr_title}>
-                    {pullRequest.pr_title}
-                  </EllipsisTextWrapper>
-                </PRName>
-
-                <Flex>
-                  <DeploymentImageContainer>
-                    {/* <InfoWrapper>
-                    <LastDeployed>
-                      #{pullRequest.pr_number} last updated xyz
-                    </LastDeployed>
-                  </InfoWrapper>
-                  <SepDot>•</SepDot> */}
-                    <MergeInfoWrapper>
-                      <MergeInfo>
-                        {pullRequest.branch_from}
-                        <i className="material-icons">arrow_forward</i>
-                        {pullRequest.branch_into}
-                      </MergeInfo>
-                    </MergeInfoWrapper>
-                  </DeploymentImageContainer>
-                </Flex>
-              </PullRequestRow>
-            );
-          })}
-        </PullRequestList>
-        {showErrorsModal && selectedPR ? (
-          <PorterYAMLErrorsModal
-            errors={porterYAMLErrors}
-            onClose={() => setShowErrorsModal(false)}
-            repo={selectedPR.repo_owner + "/" + selectedPR.repo_name}
-            branch={selectedPR.branch_from}
-          />
-        ) : null}
-        {selectedPR && porterYAMLErrors.length ? (
-          <ValidationErrorBannerWrapper>
-            <Banner type="warning">
-              We found some errors in the porter.yaml file in the&nbsp;
-              {selectedPR.branch_from}&nbsp;branch. &nbsp;
-              <LearnMoreButton onClick={() => setShowErrorsModal(true)}>
-                Learn more
-              </LearnMoreButton>
-            </Banner>
-          </ValidationErrorBannerWrapper>
-        ) : null}
-        <CreatePreviewDeploymentWrapper>
-          <SubmitButton
-            onClick={handleCreatePreviewDeployment}
-            disabled={loading || !selectedPR || porterYAMLErrors.length > 0}
-          >
-            Create preview deployment
-          </SubmitButton>
-          {selectedPR && porterYAMLErrors.length ? (
-            <RevalidatePorterYAMLSpanWrapper>
-              Please fix your porter.yaml file to continue.{" "}
-              <RevalidateSpan
-                onClick={(e) => {
-                  e.preventDefault();
-                  e.stopPropagation();
-
-                  if (!selectedPR) {
-                    return;
-                  }
-
-                  handlePRRowItemClick(selectedPR);
-                }}
-              >
-                Refresh
-              </RevalidateSpan>
-            </RevalidatePorterYAMLSpanWrapper>
-          ) : null}
-        </CreatePreviewDeploymentWrapper>
-      </>
-    );
-  };
-
   return (
     <>
       <BreadcrumbRow>
@@ -206,15 +46,18 @@ const CreateEnvironment: React.FC = () => {
         capitalize={false}
       />
       <DarkMatter />
-      {pullRequests?.length ? (
-        renderPullRequestList()
+      {/* <TabSelector
+        options={TAB_OPTIONS}
+        currentTab={currentTab.value}
+        setCurrentTab={(value: string) =>
+          setCurrentTab(TAB_OPTIONS.find((tab) => tab.value === value))
+        }
+      /> */}
+
+      {currentTab.value === "pull_requests" ? (
+        <CreatePREnvironment environmentID={environment_id} />
       ) : (
-        <>
-          <Br height="30px" />
-          <Placeholder height="370px">
-            You do not have any pull requests.
-          </Placeholder>
-        </>
+        <CreateBranchEnvironment environmentID={environment_id} />
       )}
     </>
   );
@@ -222,161 +65,11 @@ const CreateEnvironment: React.FC = () => {
 
 export default CreateEnvironment;
 
-const PullRequestList = styled.div`
-  border: 1px solid #494b4f;
-  border-radius: 5px;
-  overflow: hidden;
-`;
-
-const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
-  width: 100%;
-  padding: 15px;
-  cursor: pointer;
-  background: ${(props) => (props.isSelected ? "#ffffff11" : "#26292e")};
-  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #494b4f")};
-  :hover {
-    background: #ffffff11;
-  }
-`;
-
-const Code = styled.span`
-  font-family: monospace; ;
-`;
-
-const SepDot = styled.div`
-  color: #aaaabb66;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const DeploymentImageContainer = styled.div`
-  height: 20px;
-  font-size: 13px;
-  position: relative;
-  display: flex;
-  align-items: center;
-  font-weight: 400;
-  justify-content: center;
-  color: #ffffff66;
-  padding-left: 10px;
-`;
-
-const InfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  margin-right: 8px;
-  margin-left: 7px;
-`;
-
-const LastDeployed = styled.div`
-  font-size: 13px;
-  margin-top: -1px;
-  margin-left: 10px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb66;
-`;
-
-const MergeInfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  margin-right: 8px;
-  position: relative;
-  margin-left: 10px;
-`;
-
-const MergeInfo = styled.div`
-  font-size: 13px;
-  align-items: center;
-  color: #aaaabb66;
-  white-space: nowrap;
-  display: flex;
-  align-items: center;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  max-width: 300px;
-
-  > i {
-    font-size: 16px;
-    margin: 0 2px;
-  }
-`;
-
-const PRIcon = styled.img`
-  font-size: 20px;
-  height: 16px;
-  margin-right: 10px;
-  color: #aaaabb;
-  opacity: 50%;
-`;
-
-const PRName = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  color: #ffffff;
-  display: flex;
-  font-size: 14px;
-  align-items: center;
-  margin-bottom: 10px;
-`;
-
-const SubmitButton = styled.div`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: center;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border-radius: 5px;
-  font-weight: 500;
-  color: white;
-  height: 30px;
-  padding: 0 8px;
-  width: 200px;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "not-allowed" : "pointer"};
-
-  background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "#aaaabbee" : "#616FEEcc"};
-  :hover {
-    background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#505edddd"};
-  }
-
-  > i {
-    color: white;
-    width: 18px;
-    height: 18px;
-    font-weight: 600;
-    font-size: 12px;
-    border-radius: 20px;
-    display: flex;
-    align-items: center;
-    margin-right: 5px;
-    justify-content: center;
-  }
-`;
-
 const DarkMatter = styled.div`
   width: 100%;
   margin-top: -15px;
 `;
 
-const Br = styled.div<{ height: string }>`
-  width: 100%;
-  height: ${(props) => props.height || "2px"};
-`;
-
 const Slash = styled.div`
   margin: 0 4px;
   color: #aaaabb88;
@@ -420,42 +113,3 @@ const Breadcrumb = styled(DynamicLink)`
     background: #ffffff11;
   }
 `;
-
-const ValidationErrorBannerWrapper = styled.div`
-  margin-block: 20px;
-`;
-
-const LearnMoreButton = styled.div`
-  text-decoration: underline;
-  fontweight: bold;
-  cursor: pointer;
-`;
-
-const Message = styled.div`
-  padding: 20px;
-  background: #26292e;
-  border-radius: 5px;
-  line-height: 1.5em;
-  border: 1px solid #aaaabb33;
-  font-size: 13px;
-  margin-top: 40px;
-`;
-
-const CreatePreviewDeploymentWrapper = styled.div`
-  margin-top: 30px;
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 10px;
-`;
-
-const RevalidatePorterYAMLSpanWrapper = styled.div`
-  font-size: 13px;
-  color: #aaaabb;
-`;
-
-const RevalidateSpan = styled.span`
-  color: #aaaabb;
-  text-decoration: underline;
-  cursor: pointer;
-`;

+ 535 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx

@@ -0,0 +1,535 @@
+import React, { useContext, useMemo, useState } from "react";
+import styled from "styled-components";
+import { Context } from "shared/Context";
+import { PullRequest } from "../types";
+import Helper from "components/form-components/Helper";
+import pr_icon from "assets/pull_request_icon.svg";
+import api from "shared/api";
+import { EllipsisTextWrapper } from "../components/styled";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { getPRDeploymentList, validatePorterYAML } from "../utils";
+import Banner from "components/Banner";
+import { useRouting } from "shared/routing";
+import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal";
+import Placeholder from "components/Placeholder";
+import RadioFilter from "components/RadioFilter";
+
+import sort from "assets/sort.svg";
+import { search } from "shared/search";
+import _ from "lodash";
+import { readableDate } from "shared/string_utils";
+import dayjs from "dayjs";
+import Loading from "components/Loading";
+
+interface Props {
+  environmentID: string;
+}
+
+const CreatePREnvironment = ({ environmentID }: Props) => {
+  const queryClient = useQueryClient();
+  const router = useRouting();
+  const [searchValue, setSearchValue] = useState("");
+  const [sortOrder, setSortOrder] = useState("Newest");
+  const [showErrorsModal, setShowErrorsModal] = useState<boolean>(false);
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+
+  // Get all PRs for the current environment
+  const { isLoading: pullRequestsLoading, data: pullRequests } = useQuery<
+    PullRequest[]
+  >(
+    ["pullRequests", currentProject.id, currentCluster.id, environmentID],
+    async () => {
+      try {
+        const res = await getPRDeploymentList({
+          projectID: currentProject.id,
+          clusterID: currentCluster.id,
+          environmentID: Number(environmentID),
+        });
+
+        return res.data.pull_requests || [];
+      } catch (err) {
+        setCurrentError(err);
+      }
+    }
+  );
+
+  const [selectedPR, setSelectedPR] = useState<PullRequest>();
+  const [loading, setLoading] = useState(false);
+  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
+
+  const handleRefresh = () => {
+    queryClient.invalidateQueries({
+      queryKey: ["pullRequests"],
+    });
+  };
+
+  const handlePRRowItemClick = async (pullRequest: PullRequest) => {
+    setSelectedPR(pullRequest);
+    setLoading(true);
+
+    const res = await validatePorterYAML({
+      projectID: currentProject.id,
+      clusterID: currentCluster.id,
+      environmentID: Number(environmentID),
+      branch: pullRequest.branch_from,
+    });
+
+    setPorterYAMLErrors(res.data.errors ?? []);
+
+    setLoading(false);
+  };
+
+  const handleCreatePreviewDeployment = async () => {
+    try {
+      await api.createPreviewEnvironmentDeployment("<token>", selectedPR, {
+        cluster_id: currentCluster?.id,
+        project_id: currentProject?.id,
+      });
+
+      router.push(
+        `/preview-environments/deployments/${environmentID}/${selectedPR.repo_owner}/${selectedPR.repo_name}?status_filter=all`
+      );
+    } catch (err) {
+      setCurrentError(err);
+    }
+  };
+
+  const filteredPullRequests = useMemo(() => {
+    const filteredBySearch = search<PullRequest>(
+      pullRequests ?? [],
+      searchValue,
+      {
+        isCaseSensitive: false,
+        keys: ["pr_title", "branch_from", "branch_into"],
+      }
+    );
+
+    switch (sortOrder) {
+      case "Recently Updated":
+        return _.sortBy(filteredBySearch, "updated_at").reverse();
+      case "Newest":
+        return _.sortBy(filteredBySearch, "created_at").reverse();
+      case "Oldest":
+        return _.sortBy(filteredBySearch, "created_at");
+      case "Alphabetical":
+      default:
+        return _.sortBy(filteredBySearch, "gh_pr_name");
+    }
+  }, [pullRequests, searchValue, sortOrder]);
+
+  if (pullRequestsLoading) {
+    return (
+      <>
+        <Br height="30px" />
+        <Placeholder minHeight="50vh">
+          <Loading />
+        </Placeholder>
+      </>
+    );
+  }
+
+  if (!pullRequests.length) {
+    return (
+      <>
+        <Br height="30px" />
+        <Placeholder height="50vh">{`You do not have any pull requests.`}</Placeholder>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <Helper>
+        Select an open pull request to preview. Pull requests must contain a{" "}
+        <Code>porter.yaml</Code> file.
+      </Helper>
+      <FlexRow>
+        <Flex>
+          <SearchRowWrapper>
+            <SearchBarWrapper>
+              <i className="material-icons">search</i>
+              <SearchInput
+                value={searchValue}
+                onChange={(e: any) => {
+                  setSelectedPR(undefined);
+                  setPorterYAMLErrors([])
+                  setSearchValue(e.target.value);
+                }}
+                placeholder="Search"
+              />
+            </SearchBarWrapper>
+          </SearchRowWrapper>
+        </Flex>
+        <Flex>
+          <RefreshButton color={"#7d7d81"} onClick={handleRefresh}>
+            <i className="material-icons">refresh</i>
+          </RefreshButton>
+          <RadioFilter
+            icon={sort}
+            selected={sortOrder}
+            setSelected={setSortOrder}
+            options={[
+              { label: "Recently Updated", value: "Recently Updated" },
+              { label: "Newest", value: "Newest" },
+              { label: "Oldest", value: "Oldest" },
+              { label: "Alphabetical", value: "Alphabetical" },
+            ]}
+            name="Sort"
+          />
+        </Flex>
+      </FlexRow>
+      <Br height="10px" />
+      {filteredPullRequests?.length ? (
+        <PullRequestList>
+          {(filteredPullRequests ?? []).map(
+            (pullRequest: PullRequest, i: number) => {
+              return (
+                <PullRequestRow
+                  onClick={() => {
+                    handlePRRowItemClick(pullRequest);
+                  }}
+                  isLast={i === filteredPullRequests.length - 1}
+                  isSelected={pullRequest === selectedPR}
+                >
+                  <PRName>
+                    <PRIcon src={pr_icon} alt="pull request icon" />
+                    <EllipsisTextWrapper tooltipText={pullRequest.pr_title}>
+                      {pullRequest.pr_title}
+                    </EllipsisTextWrapper>
+                  </PRName>
+
+                  <Flex>
+                    <DeploymentImageContainer>
+                      {/* <InfoWrapper>
+                  <LastDeployed>
+                    #{pullRequest.pr_number} last updated xyz
+                  </LastDeployed>
+                </InfoWrapper>
+                <SepDot>•</SepDot> */}
+                      <MergeInfoWrapper>
+                        <MergeInfo>
+                          {pullRequest.branch_from}
+                          <i className="material-icons">arrow_forward</i>
+                          {pullRequest.branch_into}
+                        </MergeInfo>
+                        <SepDot>•</SepDot>
+                        <LastDeployed>
+                          Last updated{" "}
+                          {dayjs(pullRequest.updated_at).format(
+                            "hh:mma on MM/DD/YYYY"
+                          )}
+                        </LastDeployed>
+                      </MergeInfoWrapper>
+                    </DeploymentImageContainer>
+                  </Flex>
+                </PullRequestRow>
+              );
+            }
+          )}
+        </PullRequestList>
+      ) : (
+        <>
+          <Br height="30px" />
+          <Placeholder height="50vh">{`No pull requests match your search query.`}</Placeholder>
+        </>
+      )}
+      {showErrorsModal && selectedPR ? (
+        <PorterYAMLErrorsModal
+          errors={porterYAMLErrors}
+          onClose={() => setShowErrorsModal(false)}
+          repo={selectedPR.repo_owner + "/" + selectedPR.repo_name}
+          branch={selectedPR.branch_from}
+        />
+      ) : null}
+      {selectedPR && porterYAMLErrors.length ? (
+        <ValidationErrorBannerWrapper>
+          <Banner type="warning">
+            We found some errors in the porter.yaml file in the&nbsp;
+            {selectedPR.branch_from}&nbsp;branch. &nbsp;
+            <LearnMoreButton onClick={() => setShowErrorsModal(true)}>
+              Learn more
+            </LearnMoreButton>
+          </Banner>
+        </ValidationErrorBannerWrapper>
+      ) : null}
+      <CreatePreviewDeploymentWrapper>
+        <SubmitButton
+          onClick={handleCreatePreviewDeployment}
+          disabled={loading || !selectedPR || porterYAMLErrors.length > 0}
+        >
+          Create preview deployment
+        </SubmitButton>
+        {selectedPR && porterYAMLErrors.length ? (
+          <RevalidatePorterYAMLSpanWrapper>
+            Please fix your porter.yaml file to continue.{" "}
+            <RevalidateSpan
+              onClick={(e) => {
+                e.preventDefault();
+                e.stopPropagation();
+
+                if (!selectedPR) {
+                  return;
+                }
+
+                handlePRRowItemClick(selectedPR);
+              }}
+            >
+              Refresh
+            </RevalidateSpan>
+          </RevalidatePorterYAMLSpanWrapper>
+        ) : null}
+      </CreatePreviewDeploymentWrapper>
+    </>
+  );
+};
+
+export default CreatePREnvironment;
+
+const PullRequestList = styled.div`
+  border: 1px solid #494b4f;
+  border-radius: 5px;
+  overflow: hidden;
+  margin-top: 33px;
+`;
+
+const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
+  width: 100%;
+  padding: 15px;
+  cursor: pointer;
+  background: ${(props) => (props.isSelected ? "#ffffff11" : "#26292e")};
+  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #494b4f")};
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
+const InfoWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin-right: 8px;
+`;
+
+const Code = styled.span`
+  font-family: monospace; ;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const DeploymentImageContainer = styled.div`
+  height: 20px;
+  font-size: 13px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  font-weight: 400;
+  justify-content: center;
+  color: #ffffff66;
+  padding-left: 10px;
+`;
+
+const LastDeployed = styled.div`
+  font-size: 13px;
+  margin-top: -1px;
+  display: flex;
+  align-items: center;
+  color: #aaaabb66;
+`;
+
+const MergeInfoWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-right: 8px;
+  position: relative;
+  margin-left: 10px;
+  gap: 8px;
+`;
+
+const MergeInfo = styled.div`
+  font-size: 13px;
+  align-items: center;
+  color: #aaaabb66;
+  white-space: nowrap;
+  display: flex;
+  align-items: center;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  max-width: 300px;
+
+  > i {
+    font-size: 16px;
+    margin: 0 2px;
+  }
+`;
+
+const SepDot = styled.div`
+  color: #aaaabb66;
+`;
+
+const PRIcon = styled.img`
+  font-size: 20px;
+  height: 16px;
+  margin-right: 10px;
+  color: #aaaabb;
+  opacity: 50%;
+`;
+
+const PRName = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  color: #ffffff;
+  display: flex;
+  font-size: 14px;
+  align-items: center;
+  margin-bottom: 10px;
+`;
+
+const SubmitButton = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  font-weight: 500;
+  color: white;
+  height: 30px;
+  padding: 0 8px;
+  width: 200px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
+const Br = styled.div<{ height: string }>`
+  width: 100%;
+  height: ${(props) => props.height || "2px"};
+`;
+
+const ValidationErrorBannerWrapper = styled.div`
+  margin-block: 20px;
+`;
+
+const LearnMoreButton = styled.div`
+  text-decoration: underline;
+  fontweight: bold;
+  cursor: pointer;
+`;
+
+const CreatePreviewDeploymentWrapper = styled.div`
+  margin-top: 30px;
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+`;
+
+const RevalidatePorterYAMLSpanWrapper = styled.div`
+  font-size: 13px;
+  color: #aaaabb;
+`;
+
+const RevalidateSpan = styled.span`
+  color: #aaaabb;
+  text-decoration: underline;
+  cursor: pointer;
+`;
+
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  height: 100%;
+`;
+
+const SearchRow = styled.div`
+  display: flex;
+  align-items: center;
+  height: 30px;
+  margin-right: 10px;
+  background: #26292e;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const SearchRowWrapper = styled(SearchRow)`
+  border-radius: 5px;
+  width: 250px;
+`;
+
+const SearchBarWrapper = styled.div`
+  display: flex;
+  flex: 1;
+
+  > i {
+    color: #aaaabb;
+    padding-top: 1px;
+    margin-left: 8px;
+    font-size: 16px;
+    margin-right: 8px;
+  }
+`;
+
+const FlexRow = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 10px;
+`;
+
+const RefreshButton = styled.button`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: ${(props: { color: string }) => props.color};
+  cursor: pointer;
+  border: none;
+  width: 30px;
+  height: 30px;
+  margin-right: 15px;
+  background: none;
+  border-radius: 50%;
+  margin-left: 10px;
+  > i {
+    font-size: 20px;
+  }
+  :hover {
+    background-color: rgb(97 98 102 / 44%);
+    color: white;
+  }
+`;

+ 2 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/types.ts

@@ -55,4 +55,6 @@ export type PullRequest = {
   repo_name: string;
   branch_from: string;
   branch_into: string;
+  created_at: string;
+  updated_at: string;
 };

+ 5 - 4
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -65,10 +65,11 @@ class ProjectSettings extends Component<PropsType, StateType> {
     this.setState({ projectName: currentProject.name });
     const tabOptions = [];
     tabOptions.push({ value: "manage-access", label: "Manage access" });
-    tabOptions.push({
-      value: "billing",
-      label: "Billing",
-    });
+    // ? Disabled for now https://discord.com/channels/542888846271184896/1059277393031856208/1059277395913351258
+    // tabOptions.push({
+    //   value: "billing",
+    //   label: "Billing",
+    // });
 
     if (this.props.isAuthorized("settings", "", ["get", "delete"])) {
       // if (this.context?.hasBillingEnabled) {

+ 9 - 7
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -101,7 +101,7 @@ class Sidebar extends Component<PropsType, StateType> {
 
   renderProjectContents = () => {
     let { currentView } = this.props;
-    let { currentProject } = this.context;
+    let { currentProject, user } = this.context;
     if (currentProject) {
       return (
         <ScrollWrapper>
@@ -114,12 +114,14 @@ class Sidebar extends Component<PropsType, StateType> {
             <Img src={rocket} />
             Launch
           </NavButton>
-          {currentProject && currentProject.managed_infra_enabled && (
-            <NavButton path={"/infrastructure"}>
-              <i className="material-icons">build_circle</i>
-              Infrastructure
-            </NavButton>
-          )}
+          {currentProject &&
+            currentProject.managed_infra_enabled &&
+            user?.isPorterUser && (
+              <NavButton path={"/infrastructure"}>
+                <i className="material-icons">build_circle</i>
+                Infrastructure
+              </NavButton>
+            )}
           {this.props.isAuthorized("integrations", "", [
             "get",
             "create",

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

@@ -129,7 +129,9 @@ class ContextProvider extends Component<PropsType, StateType> {
     },
     user: null,
     setUser: (userId: number, email: string) => {
-      this.setState({ user: { userId, email } });
+      this.setState({
+        user: { userId, email, isPorterUser: email.endsWith("@porter.run") },
+      });
     },
     devOpsMode: true,
     setDevOpsMode: (devOpsMode: boolean) => {

+ 81 - 89
go.mod

@@ -3,10 +3,10 @@ module github.com/porter-dev/porter
 go 1.18
 
 require (
-	cloud.google.com/go v0.102.0
+	cloud.google.com/go v0.105.0 // indirect
 	github.com/AlecAivazis/survey/v2 v2.2.9
-	github.com/Masterminds/semver/v3 v3.1.1
-	github.com/aws/aws-sdk-go v1.43.28
+	github.com/Masterminds/semver/v3 v3.2.0
+	github.com/aws/aws-sdk-go v1.44.160
 	github.com/bradleyfalzon/ghinstallation/v2 v2.0.3
 	github.com/buildpacks/pack v0.27.0
 	github.com/cli/cli v1.11.0
@@ -25,7 +25,7 @@ require (
 	github.com/go-test/deep v1.0.7
 	github.com/golang-jwt/jwt/v4 v4.4.1 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/google/go-github/v39 v39.2.0 // indirect
+	github.com/google/go-github/v39 v39.2.0
 	github.com/google/go-github/v41 v41.0.0
 	github.com/gorilla/schema v1.2.0
 	github.com/gorilla/securecookie v1.1.1
@@ -42,52 +42,57 @@ require (
 	github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935
 	github.com/rs/zerolog v1.26.0
 	github.com/sendgrid/sendgrid-go v3.8.0+incompatible
-	github.com/spf13/cobra v1.5.0
+	github.com/spf13/cobra v1.6.1
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.10.0
-	github.com/stretchr/testify v1.8.0
-	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
-	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
-	golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26
-	google.golang.org/api v0.88.0
-	google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03
-	google.golang.org/grpc v1.49.0
+	github.com/stretchr/testify v1.8.1
+	golang.org/x/crypto v0.4.0
+	golang.org/x/net v0.4.0
+	golang.org/x/oauth2 v0.3.0
+	google.golang.org/api v0.103.0
+	google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd
+	google.golang.org/grpc v1.50.1
 	google.golang.org/protobuf v1.28.1
-	gorm.io/driver/sqlite v1.1.3
-	gorm.io/gorm v1.22.3
-	helm.sh/helm/v3 v3.9.0
-	k8s.io/api v0.24.2
-	k8s.io/apimachinery v0.24.2
-	k8s.io/cli-runtime v0.24.2
-	k8s.io/client-go v0.24.2
+	gorm.io/gorm v1.24.2
+	k8s.io/api v0.26.0
+	k8s.io/apimachinery v0.26.0
+	k8s.io/cli-runtime v0.25.2
+	k8s.io/client-go v0.26.0
 	k8s.io/helm v2.17.0+incompatible
-	k8s.io/kubectl v0.24.1
-	sigs.k8s.io/aws-iam-authenticator v0.5.8
+	k8s.io/kubectl v0.25.2
+	sigs.k8s.io/aws-iam-authenticator v0.6.1
 	sigs.k8s.io/yaml v1.3.0
 )
 
 require (
+	cloud.google.com/go/artifactregistry v1.9.0
+	cloud.google.com/go/iam v0.7.0
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1
+	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0
 	github.com/briandowns/spinner v1.18.1
+	github.com/glebarez/sqlite v1.6.0
+	github.com/open-policy-agent/opa v0.44.0
+	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
+	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d
+	github.com/xanzy/go-gitlab v0.68.0
+	go.uber.org/goleak v1.1.12
 	gopkg.in/segmentio/analytics-go.v3 v3.1.0
 	gopkg.in/yaml.v2 v2.4.0
-	gorm.io/driver/postgres v1.2.3
+	gorm.io/driver/postgres v1.4.5
+	istio.io/client-go v1.16.0
 )
 
 require (
-	cloud.google.com/go/artifactregistry v1.3.0 // indirect
-	cloud.google.com/go/compute v1.7.0 // indirect
-	cloud.google.com/go/iam v0.3.0 // indirect
+	cloud.google.com/go/compute v1.14.0 // indirect
+	cloud.google.com/go/compute/metadata v0.2.1 // indirect
+	cloud.google.com/go/longrunning v0.3.0 // indirect
 	github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0 // indirect
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
 	github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
 	github.com/OneOfOne/xxhash v1.2.8 // indirect
-	github.com/PuerkitoBio/goquery v1.5.1 // indirect
 	github.com/agnivade/levenshtein v1.1.1 // indirect
-	github.com/andybalholm/cascadia v1.1.0 // indirect
 	github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect
 	github.com/aws/aws-sdk-go-v2/config v1.15.9 // indirect
 	github.com/aws/aws-sdk-go-v2/credentials v1.12.4 // indirect
@@ -103,31 +108,29 @@ require (
 	github.com/aws/smithy-go v1.11.2 // indirect
 	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
 	github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect
-	github.com/cosmtrek/air v1.30.0 // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect
-	github.com/emicklei/go-restful/v3 v3.8.0 // indirect
+	github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
+	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
+	github.com/glebarez/go-sqlite v1.20.0 // indirect
 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
 	github.com/google/gnostic v0.6.9 // indirect
-	github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
-	github.com/mmcdole/gofeed v1.1.3 // indirect
-	github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/open-policy-agent/opa v0.44.0 // indirect
 	github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
 	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
-	github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect
-	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
+	github.com/rogpeppe/go-internal v1.9.0 // indirect
 	github.com/tchap/go-patricia/v2 v2.3.1 // indirect
-	github.com/tfkhsr/jsonschema v0.0.0-20180218143334-273afdd5a88c // indirect
-	github.com/xanzy/go-gitlab v0.68.0 // indirect
 	github.com/yashtewari/glob-intersection v0.1.0 // indirect
-	go.uber.org/goleak v1.1.12 // indirect
 	istio.io/api v0.0.0-20221109202042-b9e5d446a83d // indirect
-	istio.io/client-go v1.16.0 // indirect
+	modernc.org/libc v1.21.5 // indirect
+	modernc.org/mathutil v1.5.0 // indirect
+	modernc.org/memory v1.5.0 // indirect
+	modernc.org/sqlite v1.20.0 // indirect
 )
 
 require (
@@ -139,27 +142,22 @@ require (
 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
-	github.com/BurntSushi/toml v1.1.0 // indirect
+	github.com/BurntSushi/toml v1.2.0 // indirect
 	github.com/MakeNowJust/heredoc v1.0.0 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver v1.5.0 // indirect
-	github.com/Masterminds/sprig/v3 v3.2.2 // indirect
+	github.com/Masterminds/sprig/v3 v3.2.3 // indirect
 	github.com/Masterminds/squirrel v1.5.3 // indirect
 	github.com/Microsoft/go-winio v0.5.2 // indirect
 	github.com/Microsoft/hcsshim v0.9.4 // indirect
-	github.com/PuerkitoBio/purell v1.1.1 // indirect
-	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
 	github.com/apex/log v1.9.0 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/buildpacks/imgutil v0.0.0-20220527150729-7a271a852e31 // indirect
 	github.com/buildpacks/lifecycle v0.14.1 // indirect
-	github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
-	github.com/cespare/xxhash/v2 v2.1.2 // indirect
-	github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/chai2010/gettext-go v1.0.2 // indirect
 	github.com/cli/safeexec v1.0.0 // indirect
-	github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
-	github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
 	github.com/containerd/cgroups v1.0.3 // indirect
 	github.com/containerd/containerd v1.6.8 // indirect
 	github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
@@ -170,12 +168,9 @@ require (
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/dustin/go-humanize v1.0.0 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
-	github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
-	github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
-	github.com/fvbommel/sortorder v1.0.1 // indirect
 	github.com/gdamore/encoding v1.0.0 // indirect
 	github.com/gdamore/tcell/v2 v2.5.1 // indirect
 	github.com/ghodss/yaml v1.0.0 // indirect
@@ -191,35 +186,34 @@ require (
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/btree v1.1.2 // indirect
-	github.com/google/go-cmp v0.5.8 // indirect
+	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/google/go-containerregistry v0.9.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
-	github.com/google/uuid v1.3.0 // indirect
-	github.com/googleapis/gax-go/v2 v2.4.0 // indirect
-	github.com/googleapis/gnostic v0.5.5 // indirect
+	github.com/google/uuid v1.3.0
+	github.com/googleapis/gax-go/v2 v2.7.0 // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
 	github.com/gosuri/uitable v0.0.4 // indirect
 	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/heroku/color v0.0.6 // indirect
-	github.com/huandu/xstrings v1.3.2 // indirect
+	github.com/huandu/xstrings v1.3.3 // indirect
 	github.com/imdario/mergo v0.3.13 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.0.1 // indirect
 	github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3 // indirect
 	github.com/itchyny/timefmt-go v0.1.1 // indirect
 	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
-	github.com/jackc/pgconn v1.10.1 // indirect
+	github.com/jackc/pgconn v1.13.0 // indirect
 	github.com/jackc/pgio v1.0.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
-	github.com/jackc/pgproto3/v2 v2.2.0 // indirect
+	github.com/jackc/pgproto3/v2 v2.3.1 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
-	github.com/jackc/pgtype v1.9.0 // indirect
-	github.com/jackc/pgx/v4 v4.14.0 // indirect
+	github.com/jackc/pgtype v1.13.0 // indirect
+	github.com/jackc/pgx/v4 v4.17.2 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
-	github.com/jinzhu/now v1.1.2 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/jmoiron/sqlx v1.3.5 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
@@ -231,16 +225,16 @@ require (
 	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
 	github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
 	github.com/leodido/go-urn v1.2.0 // indirect
-	github.com/lib/pq v1.10.6 // indirect
+	github.com/lib/pq v1.10.7 // indirect
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/magiconair/properties v1.8.5 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.12 // indirect
-	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mattn/go-isatty v0.0.16 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
-	github.com/mattn/go-sqlite3 v1.14.6 // indirect
-	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
+	github.com/mattn/go-sqlite3 v1.14.16 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -256,20 +250,19 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
 	github.com/morikuni/aec v1.0.0 // indirect
-	github.com/onsi/ginkgo v1.16.4 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/runc v1.1.2 // indirect
 	github.com/opencontainers/selinux v1.10.1 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_golang v1.13.0 // indirect
-	github.com/prometheus/client_model v0.2.0 // indirect
-	github.com/prometheus/common v0.37.0 // indirect
+	github.com/prometheus/client_golang v1.14.0 // indirect
+	github.com/prometheus/client_model v0.3.0 // indirect
+	github.com/prometheus/common v0.39.0 // indirect
 	github.com/prometheus/procfs v0.8.0 // indirect
 	github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
-	github.com/rubenv/sql-migrate v1.1.2 // indirect
+	github.com/rubenv/sql-migrate v1.2.0 // indirect
 	github.com/russross/blackfriday v1.6.0 // indirect
 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
 	github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect
@@ -289,31 +282,30 @@ require (
 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
 	github.com/xlab/treeprint v1.1.0 // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
-	go.opencensus.io v0.23.0 // indirect
+	go.opencensus.io v0.24.0 // indirect
 	go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
 	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
-	golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
-	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
-	golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
-	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/sync v0.1.0 // indirect
+	golang.org/x/sys v0.3.0 // indirect
+	golang.org/x/term v0.3.0 // indirect
+	golang.org/x/text v0.5.0 // indirect
 	golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	gopkg.in/gorp.v1 v1.7.2 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.66.2 // indirect
 	gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
 	gopkg.in/src-d/go-git.v4 v4.13.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiextensions-apiserver v0.24.2 // indirect
-	k8s.io/apiserver v0.24.2 // indirect
-	k8s.io/component-base v0.24.2 // indirect
-	k8s.io/klog/v2 v2.70.0 // indirect
-	k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
-	k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
+	k8s.io/apiextensions-apiserver v0.25.2 // indirect
+	k8s.io/apiserver v0.25.2 // indirect
+	k8s.io/component-base v0.25.2 // indirect
+	k8s.io/klog/v2 v2.80.1 // indirect
+	k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
+	k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect
 	oras.land/oras-go v1.2.0 // indirect
-	sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 // indirect
-	sigs.k8s.io/kustomize/api v0.11.5 // indirect
-	sigs.k8s.io/kustomize/kyaml v0.13.7 // indirect
-	sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
+	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+	sigs.k8s.io/kustomize/api v0.12.1 // indirect
+	sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
 )

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 69 - 284
go.sum


+ 1 - 1
internal/adapter/gorm.go

@@ -8,9 +8,9 @@ import (
 
 	"gorm.io/gorm/logger"
 
+	"github.com/glebarez/sqlite"
 	"github.com/porter-dev/porter/api/server/shared/config/env"
 	"gorm.io/driver/postgres"
-	"gorm.io/driver/sqlite"
 	"gorm.io/gorm"
 )
 

+ 4 - 4
internal/helm/agent.go

@@ -10,11 +10,11 @@ import (
 
 	"github.com/pkg/errors"
 	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/stefanmcshane/helm/pkg/action"
+	"github.com/stefanmcshane/helm/pkg/chart"
+	"github.com/stefanmcshane/helm/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/storage/driver"
 	"golang.org/x/oauth2"
-	"helm.sh/helm/v3/pkg/action"
-	"helm.sh/helm/v3/pkg/chart"
-	"helm.sh/helm/v3/pkg/release"
-	"helm.sh/helm/v3/pkg/storage/driver"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/helm/pkg/chartutil"

+ 3 - 3
internal/helm/agent_test.go

@@ -3,14 +3,14 @@ package helm_test
 import (
 	"testing"
 
-	"helm.sh/helm/v3/pkg/storage/driver"
+	"github.com/stefanmcshane/helm/pkg/storage/driver"
 
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/pkg/logger"
 
-	"helm.sh/helm/v3/pkg/chart"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/chart"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 func newAgentFixture(t *testing.T, namespace string) *helm.Agent {

+ 5 - 5
internal/helm/config.go

@@ -9,12 +9,12 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/pkg/logger"
+	"github.com/stefanmcshane/helm/pkg/action"
+	"github.com/stefanmcshane/helm/pkg/chartutil"
+	"github.com/stefanmcshane/helm/pkg/kube"
+	kubefake "github.com/stefanmcshane/helm/pkg/kube/fake"
+	"github.com/stefanmcshane/helm/pkg/storage"
 	"golang.org/x/oauth2"
-	"helm.sh/helm/v3/pkg/action"
-	"helm.sh/helm/v3/pkg/chartutil"
-	"helm.sh/helm/v3/pkg/kube"
-	kubefake "helm.sh/helm/v3/pkg/kube/fake"
-	"helm.sh/helm/v3/pkg/storage"
 	k8s "k8s.io/client-go/kubernetes"
 )
 

+ 2 - 2
internal/helm/loader/loader.go

@@ -12,8 +12,8 @@ import (
 	"sigs.k8s.io/yaml"
 
 	"github.com/porter-dev/porter/api/types"
-	"helm.sh/helm/v3/pkg/chart"
-	chartloader "helm.sh/helm/v3/pkg/chart/loader"
+	"github.com/stefanmcshane/helm/pkg/chart"
+	chartloader "github.com/stefanmcshane/helm/pkg/chart/loader"
 )
 
 // RepoIndexToPorterChartList converts an index file to a list of porter charts

+ 1 - 1
internal/helm/postrenderer.go

@@ -13,9 +13,9 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
+	"github.com/stefanmcshane/helm/pkg/postrender"
 	"golang.org/x/oauth2"
 	"gopkg.in/yaml.v2"
-	"helm.sh/helm/v3/pkg/postrender"
 
 	"github.com/docker/distribution/reference"
 )

+ 1 - 1
internal/helm/repo/repo.go

@@ -6,7 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/models"
-	"helm.sh/helm/v3/pkg/chart"
+	"github.com/stefanmcshane/helm/pkg/chart"
 
 	"github.com/porter-dev/porter/internal/repository"
 )

+ 2 - 2
internal/helm/storage.go

@@ -16,8 +16,8 @@ package helm
 import (
 	"github.com/porter-dev/porter/pkg/logger"
 
-	"helm.sh/helm/v3/pkg/storage"
-	"helm.sh/helm/v3/pkg/storage/driver"
+	"github.com/stefanmcshane/helm/pkg/storage"
+	"github.com/stefanmcshane/helm/pkg/storage/driver"
 	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
 )
 

+ 3 - 3
internal/helm/storage_test.go

@@ -4,13 +4,13 @@ import (
 	"reflect"
 	"testing"
 
-	"helm.sh/helm/v3/pkg/chart"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/chart"
+	"github.com/stefanmcshane/helm/pkg/release"
 
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/pkg/logger"
-	"helm.sh/helm/v3/pkg/storage"
+	"github.com/stefanmcshane/helm/pkg/storage"
 	"k8s.io/client-go/kubernetes/fake"
 )
 

+ 1 - 1
internal/kubernetes/agent.go

@@ -46,7 +46,7 @@ import (
 	"k8s.io/client-go/tools/remotecommand"
 	"k8s.io/kubectl/pkg/scheme"
 
-	rspb "helm.sh/helm/v3/pkg/release"
+	rspb "github.com/stefanmcshane/helm/pkg/release"
 
 	istiov1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
 	versionedclient "istio.io/client-go/pkg/clientset/versioned"

+ 1 - 1
internal/kubernetes/envgroup/create.go

@@ -9,7 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/kubernetes"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 	v1 "k8s.io/api/core/v1"
 )
 

+ 1 - 1
internal/opa/opa.go

@@ -13,7 +13,7 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/pkg/logger"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"

+ 1 - 1
internal/repository/gorm/api_token.go

@@ -27,7 +27,7 @@ func (repo *APITokenRepository) CreateAPIToken(a *models.APIToken) (*models.APIT
 func (repo *APITokenRepository) ListAPITokensByProjectID(projectID uint) ([]*models.APIToken, error) {
 	tokens := []*models.APIToken{}
 
-	if err := repo.db.Where("project_id = ? AND NOT revoked", projectID, true).Find(&tokens).Error; err != nil {
+	if err := repo.db.Where("project_id = ? AND NOT revoked", projectID).Find(&tokens).Error; err != nil {
 		return nil, err
 	}
 

+ 31 - 0
internal/repository/gorm/api_token_test.go

@@ -0,0 +1,31 @@
+package gorm_test
+
+import (
+	"testing"
+)
+
+func TestListAPITokensByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_tokens.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initAPITokens(tester, t)
+	initMultiUser(tester, t)
+	defer cleanup(tester, t)
+
+	projectID := tester.initProjects[0].Model.ID
+	found, err := tester.repo.APIToken().ListAPITokensByProjectID(projectID)
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(found) != 1 {
+		t.Errorf("expected to find 1 row, found %d", len(found))
+	}
+
+	if found[0].ID != 1 {
+		t.Errorf("expected found to be %d but got: %d", 1, found[0].ID)
+	}
+}

+ 20 - 0
internal/repository/gorm/helpers_test.go

@@ -41,6 +41,7 @@ type tester struct {
 	initAWSs       []*ints.AWSIntegration
 	initAllowlist  []*models.Allowlist
 	initTags       []*models.Tag
+	initAPITokens  []*models.APIToken
 }
 
 func setupTestEnv(tester *tester, t *testing.T) {
@@ -79,6 +80,7 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.Onboarding{},
 		&models.Allowlist{},
 		&models.Tag{},
+		&models.APIToken{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},
@@ -638,3 +640,21 @@ func initKubeEvents(tester *tester, t *testing.T) {
 
 	tester.initKubeEvents = initEvents
 }
+
+func initAPITokens(tester *tester, t *testing.T) {
+	t.Helper()
+
+	ti := time.Now().Add(10 * time.Minute)
+	tokens := &models.APIToken{
+		UniqueID:        "1",
+		ProjectID:       tester.initProjects[0].Model.ID,
+		CreatedByUserID: 1,
+		Expiry:          &ti,
+		Revoked:         false,
+		Name:            "test-key",
+	}
+
+	tester.db.Create(&tokens)
+
+	tester.initAPITokens = append(tester.initAPITokens, tokens)
+}

+ 1 - 1
internal/stacks/hooks.go

@@ -6,8 +6,8 @@ import (
 
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/stefanmcshane/helm/pkg/release"
 	"gorm.io/gorm"
-	"helm.sh/helm/v3/pkg/release"
 )
 
 func UpdateHelmRevision(config *config.Config, projID, clusterID uint, rel *release.Release) error {

+ 1 - 1
internal/templater/helm/manifests/reader.go

@@ -6,7 +6,7 @@ import (
 
 	"github.com/porter-dev/porter/internal/templater"
 	"github.com/porter-dev/porter/internal/templater/utils"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 	"sigs.k8s.io/yaml"
 )
 

+ 2 - 2
internal/templater/helm/values/reader.go

@@ -6,8 +6,8 @@ import (
 	"github.com/porter-dev/porter/internal/templater"
 	"github.com/porter-dev/porter/internal/templater/utils"
 
-	"helm.sh/helm/v3/pkg/chart"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/chart"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 // TemplateReader implements the templater.TemplateReader for reading from

+ 1 - 1
internal/templater/helm/values/writer.go

@@ -4,7 +4,7 @@ import (
 	"fmt"
 
 	"github.com/porter-dev/porter/internal/helm"
-	"helm.sh/helm/v3/pkg/chart"
+	"github.com/stefanmcshane/helm/pkg/chart"
 )
 
 // TemplateWriter upgrades and installs charts by setting Helm values

+ 2 - 2
internal/templater/parser/parser.go

@@ -10,8 +10,8 @@ import (
 	"github.com/porter-dev/porter/internal/templater"
 	"github.com/porter-dev/porter/internal/templater/infra"
 	"github.com/porter-dev/porter/internal/templater/utils"
-	"helm.sh/helm/v3/pkg/chart"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/chart"
+	"github.com/stefanmcshane/helm/pkg/release"
 	"k8s.io/client-go/dynamic"
 	"sigs.k8s.io/yaml"
 

+ 0 - 13
provisioner/integrations/storage/s3/s3.go

@@ -20,7 +20,6 @@ type S3StorageClient struct {
 	bucket        string
 	encryptionKey *[32]byte
 }
-
 type S3Options struct {
 	AWSRegion      string
 	AWSAccessKeyID string
@@ -57,13 +56,11 @@ func NewS3StorageClient(opts *S3Options) (*S3StorageClient, error) {
 		client:        s3.New(sess),
 	}, nil
 }
-
 func (s *S3StorageClient) WriteFile(infra *models.Infra, name string, fileBytes []byte, shouldEncrypt bool) error {
 	body := fileBytes
 	var err error
 	if shouldEncrypt {
 		body, err = encryption.Encrypt(fileBytes, s.encryptionKey)
-
 		if err != nil {
 			return err
 		}
@@ -74,7 +71,6 @@ func (s *S3StorageClient) WriteFile(infra *models.Infra, name string, fileBytes
 		Bucket: &s.bucket,
 		Key:    aws.String(getKeyFromInfra(infra, name)),
 	})
-
 	return err
 }
 
@@ -83,7 +79,6 @@ func (s *S3StorageClient) WriteFileWithKey(fileBytes []byte, shouldEncrypt bool,
 	var err error
 	if shouldEncrypt {
 		body, err = encryption.Encrypt(fileBytes, s.encryptionKey)
-
 		if err != nil {
 			return err
 		}
@@ -94,7 +89,6 @@ func (s *S3StorageClient) WriteFileWithKey(fileBytes []byte, shouldEncrypt bool,
 		Bucket: &s.bucket,
 		Key:    aws.String(key),
 	})
-
 	return err
 }
 
@@ -119,19 +113,14 @@ func (s *S3StorageClient) ReadFile(infra *models.Infra, name string, shouldDecry
 
 	if shouldDecrypt {
 		var encryptedData bytes.Buffer
-
 		_, err = encryptedData.ReadFrom(output.Body)
-
 		if err != nil {
 			return nil, err
 		}
-
 		data, err := encryption.Decrypt(encryptedData.Bytes(), s.encryptionKey)
-
 		if err != nil {
 			return nil, err
 		}
-
 		return data, nil
 	} else {
 		return io.ReadAll(output.Body)
@@ -143,11 +132,9 @@ func (s *S3StorageClient) DeleteFile(infra *models.Infra, name string) error {
 		Bucket: &s.bucket,
 		Key:    aws.String(getKeyFromInfra(infra, name)),
 	})
-
 	if err != nil {
 		return err
 	}
-
 	return nil
 }
 

+ 0 - 109
provisioner/server/handlers/state/create_resource.go

@@ -36,61 +36,45 @@ func NewCreateResourceHandler(
 		decoderValidator: shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
 	}
 }
-
 func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	// read the infra from the attached scope
 	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
 	operation, _ := r.Context().Value(types.OperationScope).(*models.Operation)
-
 	req := &ptypes.CreateResourceRequest{}
-
 	if ok := c.decoderValidator.DecodeAndValidate(w, r, req); !ok {
 		return
 	}
-
 	// update the operation to indicate completion
 	operation.Status = "completed"
-
 	operation, err := c.Config.Repo.Infra().UpdateOperation(operation)
-
 	if err != nil {
 		apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		return
 	}
-
 	// push to the operation stream
 	err = redis_stream.SendOperationCompleted(c.Config.RedisClient, infra, operation)
-
 	if err != nil {
 		apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		return
 	}
-
 	// push to the global stream
 	err = redis_stream.PushToGlobalStream(c.Config.RedisClient, infra, operation, "created")
-
 	if err != nil {
 		apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		return
 	}
-
 	// update the infra to indicate completion
 	infra.Status = "created"
-
 	infra, err = c.Config.Repo.Infra().UpdateInfra(infra)
-
 	if err != nil {
 		apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		return
 	}
-
 	// switch on the kind of resource and write the corresponding objects to the database
 	switch req.Kind {
 	case string(types.InfraEKS), string(types.InfraDOKS), string(types.InfraGKE), string(types.InfraAKS):
 		var cluster *models.Cluster
-
 		cluster, err = createCluster(c.Config, infra, operation, req.Output)
-
 		if cluster != nil {
 			c.Config.AnalyticsClient.Track(analytics.ClusterProvisioningSuccessTrack(
 				&analytics.ClusterProvisioningSuccessTrackOpts{
@@ -115,7 +99,6 @@ func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	case string(types.InfraACR):
 		_, err = createACRRegistry(c.Config, infra, operation, req.Output)
 	}
-
 	if err != nil {
 		apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		return
@@ -129,10 +112,8 @@ func createECRRegistry(config *config.Config, infra *models.Infra, operation *mo
 		InfraID:          infra.ID,
 		Name:             output["name"].(string),
 	}
-
 	// parse raw data into ECR type
 	awsInt, err := config.Repo.AWSIntegration().ReadAWSIntegration(reg.ProjectID, reg.AWSIntegrationID)
-
 	if err != nil {
 		return nil, err
 	}
@@ -150,33 +131,24 @@ func createECRRegistry(config *config.Config, infra *models.Infra, operation *mo
 	if err != nil {
 		return nil, err
 	}
-
 	reg.URL = *authOutput.AuthorizationData[0].ProxyEndpoint
-
 	reg, err = config.Repo.Registry().CreateRegistry(reg)
-
 	if err != nil {
 		return nil, err
 	}
-
 	return reg, nil
 }
-
 func createRDSDatabase(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Database, error) {
 	// check for infra id being 0 as a safeguard so that all non-provisioned
 	// clusters are not matched by read
 	if infra.ID == 0 {
 		return nil, fmt.Errorf("infra id cannot be 0")
 	}
-
 	var database *models.Database
 	var err error
 	var isNotFound bool
-
 	database, err = config.Repo.Database().ReadDatabaseByInfraID(infra.ProjectID, infra.ID)
-
 	isNotFound = err != nil && errors.Is(err, gorm.ErrRecordNotFound)
-
 	if isNotFound {
 		database = &models.Database{
 			ProjectID: infra.ProjectID,
@@ -187,127 +159,95 @@ func createRDSDatabase(config *config.Config, infra *models.Infra, operation *mo
 	} else if err != nil {
 		return nil, err
 	}
-
 	database.InstanceID = output["rds_instance_id"].(string)
 	database.InstanceEndpoint = output["rds_connection_endpoint"].(string)
 	database.InstanceName = output["rds_instance_name"].(string)
-
 	if isNotFound {
 		database, err = config.Repo.Database().CreateDatabase(database)
 	} else {
 		database, err = config.Repo.Database().UpdateDatabase(database)
 	}
-
 	infra.DatabaseID = database.ID
 	infra, err = config.Repo.Infra().UpdateInfra(infra)
-
 	if err != nil {
 		return nil, err
 	}
 	lastApplied := make(map[string]interface{})
-
 	err = json.Unmarshal(operation.LastApplied, &lastApplied)
-
 	if err != nil {
 		return nil, err
 	}
-
 	err = createRDSEnvGroup(config, infra, database, lastApplied)
-
 	if err != nil {
 		return nil, err
 	}
-
 	return database, nil
 }
-
 func createS3Bucket(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) error {
 	lastApplied := make(map[string]interface{})
-
 	err := json.Unmarshal(operation.LastApplied, &lastApplied)
-
 	if err != nil {
 		return err
 	}
-
 	return createS3EnvGroup(config, infra, lastApplied, output)
 }
-
 func createCluster(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Cluster, error) {
 	// check for infra id being 0 as a safeguard so that all non-provisioned
 	// clusters are not matched by read
 	if infra.ID == 0 {
 		return nil, fmt.Errorf("infra id cannot be 0")
 	}
-
 	var cluster *models.Cluster
 	var err error
 	var isNotFound bool
-
 	// look for cluster matching infra in database; if the cluster already exists, update the cluster but
 	// don't add it again
 	cluster, err = config.Repo.Cluster().ReadClusterByInfraID(infra.ProjectID, infra.ID)
-
 	isNotFound = err != nil && errors.Is(err, gorm.ErrRecordNotFound)
-
 	if isNotFound {
 		cluster = getNewCluster(infra)
 	} else if err != nil {
 		return nil, err
 	}
-
 	caData, err := transformClusterCAData([]byte(output["cluster_ca_data"].(string)))
-
 	if err != nil {
 		return nil, err
 	}
-
 	// if cluster_token is output and infra is azure, update the azure integration
 	if _, exists := output["cluster_token"]; exists && infra.AzureIntegrationID != 0 {
 		azInt, err := config.Repo.AzureIntegration().ReadAzureIntegration(infra.ProjectID, infra.AzureIntegrationID)
-
 		if err != nil {
 			return nil, err
 		}
-
 		azInt.AKSPassword = []byte(output["cluster_token"].(string))
-
 		azInt, err = config.Repo.AzureIntegration().OverwriteAzureIntegration(azInt)
-
 		if err != nil {
 			return nil, err
 		}
 	}
-
 	// only update the cluster name if this is during creation - we don't want to overwrite the cluster name
 	// which may have been manually set
 	if isNotFound {
 		cluster.Name = output["cluster_name"].(string)
 	}
-
 	cluster.Server = output["cluster_endpoint"].(string)
 	cluster.CertificateAuthorityData = caData
-
 	if isNotFound {
 		cluster, err = config.Repo.Cluster().CreateCluster(cluster)
 	} else {
 		cluster, err = config.Repo.Cluster().UpdateCluster(cluster)
 	}
-
 	if err != nil {
 		return nil, err
 	}
-
 	return cluster, nil
 }
-
 func getNewCluster(infra *models.Infra) *models.Cluster {
 	res := &models.Cluster{
 		ProjectID:           infra.ProjectID,
 		InfraID:             infra.ID,
 		MonitorHelmReleases: true,
 	}
-
 	switch infra.Kind {
 	case types.InfraEKS:
 		res.AuthMechanism = models.AWS
@@ -322,30 +262,22 @@ func getNewCluster(infra *models.Infra) *models.Cluster {
 		res.AuthMechanism = models.Azure
 		res.AzureIntegrationID = infra.AzureIntegrationID
 	}
-
 	return res
 }
-
 func transformClusterCAData(ca []byte) ([]byte, error) {
 	re := regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`)
-
 	// if it matches the base64 regex, decode it
 	caData := string(ca)
-
 	if re.MatchString(caData) {
 		decoded, err := base64.StdEncoding.DecodeString(caData)
-
 		if err != nil {
 			return nil, err
 		}
-
 		return []byte(decoded), nil
 	}
-
 	// otherwise just return the CA
 	return ca, nil
 }
-
 func createDOCRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
 	reg := &models.Registry{
 		ProjectID:       infra.ProjectID,
@@ -354,10 +286,8 @@ func createDOCRRegistry(config *config.Config, infra *models.Infra, operation *m
 		URL:             output["url"].(string),
 		Name:            output["name"].(string),
 	}
-
 	return config.Repo.Registry().CreateRegistry(reg)
 }
-
 func createGCRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
 	reg := &models.Registry{
 		ProjectID:        infra.ProjectID,
@@ -366,10 +296,8 @@ func createGCRRegistry(config *config.Config, infra *models.Infra, operation *mo
 		URL:              output["url"].(string),
 		Name:             "gcr-registry",
 	}
-
 	return config.Repo.Registry().CreateRegistry(reg)
 }
-
 func createGARRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
 	reg := &models.Registry{
 		ProjectID:        infra.ProjectID,
@@ -378,10 +306,8 @@ func createGARRegistry(config *config.Config, infra *models.Infra, operation *mo
 		URL:              output["url"].(string),
 		Name:             "gar-registry",
 	}
-
 	return config.Repo.Registry().CreateRegistry(reg)
 }
-
 func createACRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
 	reg := &models.Registry{
 		ProjectID:          infra.ProjectID,
@@ -390,38 +316,29 @@ func createACRRegistry(config *config.Config, infra *models.Infra, operation *mo
 		URL:                output["url"].(string),
 		Name:               output["name"].(string),
 	}
-
 	return config.Repo.Registry().CreateRegistry(reg)
 }
-
 func createRDSEnvGroup(config *config.Config, infra *models.Infra, database *models.Database, lastApplied map[string]interface{}) error {
 	cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)
-
 	if err != nil {
 		return err
 	}
-
 	ooc := &kubernetes.OutOfClusterConfig{
 		Repo:              config.Repo,
 		DigitalOceanOAuth: config.DOConf,
 		Cluster:           cluster,
 	}
-
 	agent, err := kubernetes.GetAgentOutOfClusterConfig(ooc)
-
 	if err != nil {
 		return fmt.Errorf("failed to get agent: %s", err.Error())
 	}
-
 	// split the instance endpoint on the port
 	port := "5432"
 	host := database.InstanceEndpoint
-
 	if strArr := strings.Split(database.InstanceEndpoint, ":"); len(strArr) == 2 {
 		host = strArr[0]
 		port = strArr[1]
 	}
-
 	_, err = envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
 		Name:      fmt.Sprintf("rds-credentials-%s", lastApplied["db_name"].(string)),
 		Namespace: "default",
@@ -433,61 +350,45 @@ func createRDSEnvGroup(config *config.Config, infra *models.Infra, database *mod
 			"PGUSER":     lastApplied["db_user"].(string),
 		},
 	})
-
 	if err != nil {
 		return fmt.Errorf("failed to create RDS env group: %s", err.Error())
 	}
-
 	return nil
 }
-
 func deleteRDSEnvGroup(config *config.Config, infra *models.Infra, lastApplied map[string]interface{}) error {
 	cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)
-
 	if err != nil {
 		return err
 	}
-
 	ooc := &kubernetes.OutOfClusterConfig{
 		Repo:              config.Repo,
 		DigitalOceanOAuth: config.DOConf,
 		Cluster:           cluster,
 	}
-
 	agent, err := kubernetes.GetAgentOutOfClusterConfig(ooc)
-
 	if err != nil {
 		return fmt.Errorf("failed to get agent: %s", err.Error())
 	}
-
 	err = envgroup.DeleteEnvGroup(agent, fmt.Sprintf("rds-credentials-%s", lastApplied["db_name"].(string)), "default")
-
 	if err != nil {
 		return fmt.Errorf("failed to create RDS env group: %s", err.Error())
 	}
-
 	return nil
 }
-
 func createS3EnvGroup(config *config.Config, infra *models.Infra, lastApplied map[string]interface{}, output map[string]interface{}) error {
 	cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)
-
 	if err != nil {
 		return err
 	}
-
 	ooc := &kubernetes.OutOfClusterConfig{
 		Repo:              config.Repo,
 		DigitalOceanOAuth: config.DOConf,
 		Cluster:           cluster,
 	}
-
 	agent, err := kubernetes.GetAgentOutOfClusterConfig(ooc)
-
 	if err != nil {
 		return fmt.Errorf("failed to get agent: %s", err.Error())
 	}
-
 	// split the instance endpoint on the port
 	_, err = envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
 		Name:      fmt.Sprintf("s3-credentials-%s", lastApplied["bucket_name"].(string)),
@@ -499,38 +400,28 @@ func createS3EnvGroup(config *config.Config, infra *models.Infra, lastApplied ma
 			"S3_BUCKET_NAME":       output["s3_bucket_name"].(string),
 		},
 	})
-
 	if err != nil {
 		return fmt.Errorf("failed to create S3 env group: %s", err.Error())
 	}
-
 	return nil
 }
-
 func deleteS3EnvGroup(config *config.Config, infra *models.Infra, lastApplied map[string]interface{}) error {
 	cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)
-
 	if err != nil {
 		return err
 	}
-
 	ooc := &kubernetes.OutOfClusterConfig{
 		Repo:              config.Repo,
 		DigitalOceanOAuth: config.DOConf,
 		Cluster:           cluster,
 	}
-
 	agent, err := kubernetes.GetAgentOutOfClusterConfig(ooc)
-
 	if err != nil {
 		return fmt.Errorf("failed to get agent: %s", err.Error())
 	}
-
 	err = envgroup.DeleteEnvGroup(agent, fmt.Sprintf("s3-credentials-%s", lastApplied["bucket_name"].(string)), "default")
-
 	if err != nil {
 		return fmt.Errorf("failed to create RDS env group: %s", err.Error())
 	}
-
 	return nil
 }

+ 1 - 1
workers/jobs/helm_revisions_count_tracker.go

@@ -41,9 +41,9 @@ import (
 	"github.com/porter-dev/porter/internal/repository"
 	rcreds "github.com/porter-dev/porter/internal/repository/credentials"
 	rgorm "github.com/porter-dev/porter/internal/repository/gorm"
+	"github.com/stefanmcshane/helm/pkg/releaseutil"
 	"golang.org/x/oauth2"
 	"gorm.io/gorm"
-	"helm.sh/helm/v3/pkg/releaseutil"
 )
 
 var stepSize int = 20

+ 1 - 1
workers/utils/retry_helm_agent.go

@@ -11,7 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/pkg/logger"
-	"helm.sh/helm/v3/pkg/release"
+	"github.com/stefanmcshane/helm/pkg/release"
 )
 
 type RetryHelmAgent struct {

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.