Преглед на файлове

Merge branch 'gitlab-integration' of github.com:porter-dev/porter into gitlab-integration

jnfrati преди 3 години
родител
ревизия
ab5980a367
променени са 53 файла, в които са добавени 594 реда и са изтрити 486 реда
  1. 0 2
      .github/workflows/dev.yaml
  2. 0 1
      .github/workflows/staging.yaml
  3. 29 0
      api/client/k8s.go
  4. 64 0
      api/server/authz/gitlab_integration.go
  5. 2 0
      api/server/authz/policy.go
  6. 2 2
      api/server/handlers/environment/create.go
  7. 2 2
      api/server/handlers/environment/create_deployment.go
  8. 2 2
      api/server/handlers/environment/delete.go
  9. 1 1
      api/server/handlers/environment/enable_pull_request.go
  10. 2 2
      api/server/handlers/environment/finalize_deployment.go
  11. 2 2
      api/server/handlers/environment/get_deployment.go
  12. 2 2
      api/server/handlers/environment/list_deployments.go
  13. 2 2
      api/server/handlers/environment/update_deployment.go
  14. 2 2
      api/server/handlers/environment/update_deployment_status.go
  15. 3 2
      api/server/handlers/gitinstallation/get_buildpack.go
  16. 3 2
      api/server/handlers/gitinstallation/get_contents.go
  17. 3 2
      api/server/handlers/gitinstallation/get_procfile.go
  18. 3 2
      api/server/handlers/gitinstallation/get_tarball_url.go
  19. 0 42
      api/server/handlers/gitinstallation/helpers.go
  20. 2 1
      api/server/handlers/gitinstallation/list_branches.go
  21. 1 1
      api/server/handlers/gitinstallation/rerun_workflow.go
  22. 7 0
      api/server/handlers/project_integration/create_gitlab.go
  23. 9 68
      api/server/handlers/project_integration/get_gitlab_repo_buildpack.go
  24. 9 68
      api/server/handlers/project_integration/get_gitlab_repo_contents.go
  25. 9 68
      api/server/handlers/project_integration/get_gitlab_repo_procfile.go
  26. 7 65
      api/server/handlers/project_integration/list_gitlab_repo_branches.go
  27. 47 49
      api/server/handlers/project_integration/list_gitlab_repos.go
  28. 18 9
      api/server/router/project_integration.go
  29. 5 0
      api/server/router/router.go
  30. 43 0
      api/server/shared/commonutils/git_utils.go
  31. 3 0
      api/server/shared/config/env/envconfs.go
  32. 2 0
      api/server/shared/config/metadata.go
  33. 13 12
      api/types/policy.go
  34. 1 0
      api/types/request.go
  35. 15 0
      cli/cmd/config.go
  36. 41 2
      cli/cmd/config/config.go
  37. 1 1
      cli/cmd/portforward.go
  38. 1 1
      cli/cmd/run.go
  39. 24 0
      dashboard/package-lock.json
  40. 104 48
      dashboard/src/components/repo-selector/RepoList.tsx
  41. 1 0
      dashboard/src/main/Main.tsx
  42. 1 1
      dashboard/src/main/home/integrations/GitlabIntegrationList.tsx
  43. 12 7
      dashboard/src/main/home/integrations/Integrations.tsx
  44. 5 3
      dashboard/src/main/home/integrations/create-integration/GitlabForm.tsx
  45. 2 2
      dashboard/src/main/home/launch/launch-flow/SourcePage.tsx
  46. 27 2
      dashboard/src/main/home/project-settings/ProjectSettings.tsx
  47. 6 0
      dashboard/src/shared/Context.tsx
  48. 2 2
      dashboard/src/shared/common.tsx
  49. 30 0
      dashboard/src/shared/hooks/useOutsideAlerter.ts
  50. 2 0
      dashboard/src/shared/types.tsx
  51. 6 5
      go.mod
  52. 13 0
      go.sum
  53. 1 1
      internal/integrations/ci/actions/steps.go

+ 0 - 2
.github/workflows/dev.yaml

@@ -40,7 +40,6 @@ jobs:
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_ENV=frontend-development
-          ENABLE_GITLAB=true
           EOL
       - name: Build
         run: |
@@ -118,7 +117,6 @@ jobs:
         ENABLE_SENTRY=true
         SENTRY_DSN=${{secrets.SENTRY_DSN}}
         SENTRY_ENV=frontend-development
-        ENABLE_GITLAB=true
         EOL
     - name: Build
       run: |

+ 0 - 1
.github/workflows/staging.yaml

@@ -47,7 +47,6 @@ jobs:
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_ENV=frontend-staging
-          ENABLE_GITLAB=true
           EOL
       - name: Build
         run: |

+ 29 - 0
api/client/k8s.go

@@ -3,8 +3,12 @@ package client
 import (
 	"context"
 	"fmt"
+	"io"
+	"os"
 
+	"github.com/fatih/color"
 	"github.com/porter-dev/porter/api/types"
+
 	v1 "k8s.io/api/batch/v1"
 )
 
@@ -55,9 +59,34 @@ func (c *Client) GetKubeconfig(
 	ctx context.Context,
 	projectID uint,
 	clusterID uint,
+	localKubeconfigPath string,
 ) (*types.GetTemporaryKubeconfigResponse, error) {
 	resp := &types.GetTemporaryKubeconfigResponse{}
 
+	if localKubeconfigPath != "" {
+		color.New(color.FgBlue).Printf("using local kubeconfig: %s\n", localKubeconfigPath)
+
+		if _, err := os.Stat(localKubeconfigPath); !os.IsNotExist(err) {
+			file, err := os.Open(localKubeconfigPath)
+
+			if err != nil {
+				return nil, err
+			}
+
+			data, err := io.ReadAll(file)
+
+			if err != nil {
+				return nil, err
+			}
+
+			resp.Kubeconfig = append(resp.Kubeconfig, data...)
+
+			return resp, nil
+		}
+	}
+
+	color.New(color.FgBlue).Println("using remote kubeconfig")
+
 	err := c.getRequest(
 		fmt.Sprintf(
 			"/projects/%d/clusters/%d/kubeconfig",

+ 64 - 0
api/server/authz/gitlab_integration.go

@@ -0,0 +1,64 @@
+package authz
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
+)
+
+type GitlabIntegrationScopedFactory struct {
+	config *config.Config
+}
+
+func NewGitlabIntegrationScopedFactory(
+	config *config.Config,
+) *GitlabIntegrationScopedFactory {
+	return &GitlabIntegrationScopedFactory{config}
+}
+
+func (p *GitlabIntegrationScopedFactory) Middleware(next http.Handler) http.Handler {
+	return &GitlabIntegrationScopedMiddleware{next, p.config}
+}
+
+type GitlabIntegrationScopedMiddleware struct {
+	next   http.Handler
+	config *config.Config
+}
+
+func (p *GitlabIntegrationScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// read the project to check scopes
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	// get the integration id from the URL param context
+	reqScopes, _ := r.Context().Value(types.RequestScopeCtxKey).(map[types.PermissionScope]*types.RequestAction)
+	integrationID := reqScopes[types.GitlabIntegrationScope].Resource.UInt
+	gi, err := p.config.Repo.GitlabIntegration().ReadGitlabIntegration(proj.ID, integrationID)
+
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(
+				fmt.Errorf("gitlab integration not found with id %d", integrationID),
+			), true)
+
+			return
+		}
+
+		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
+		return
+	}
+
+	ctx := NewGitlabIntegrationContext(r.Context(), gi)
+	r = r.Clone(ctx)
+	p.next.ServeHTTP(w, r)
+}
+
+func NewGitlabIntegrationContext(ctx context.Context, gi *ints.GitlabIntegration) context.Context {
+	return context.WithValue(ctx, types.GitlabIntegrationScope, gi)
+}

+ 2 - 0
api/server/authz/policy.go

@@ -132,6 +132,8 @@ func getRequestActionForEndpoint(
 			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamReleaseName)
 		case types.InviteScope:
 			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamInviteID)
+		case types.GitlabIntegrationScope:
+			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamIntegrationID)
 		}
 
 		if reqErr != nil {

+ 2 - 2
api/server/handlers/environment/create.go

@@ -9,9 +9,9 @@ import (
 	ghinstallation "github.com/bradleyfalzon/ghinstallation/v2"
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/auth/token"
@@ -41,7 +41,7 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 2 - 2
api/server/handlers/environment/create_deployment.go

@@ -8,9 +8,9 @@ import (
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -38,7 +38,7 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 2 - 2
api/server/handlers/environment/delete.go

@@ -5,9 +5,9 @@ import (
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
@@ -36,7 +36,7 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 1 - 1
api/server/handlers/environment/enable_pull_request.go

@@ -101,7 +101,7 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	namespace := fmt.Sprintf("pr-%d-%s", request.Number, strings.ReplaceAll(env.GitRepoName, "_", "-"))
+	namespace := fmt.Sprintf("pr-%d-%s", request.Number, strings.ToLower(strings.ReplaceAll(env.GitRepoName, "_", "-")))
 
 	// create the deployment
 	depl, err := c.Repo().Environment().CreateDeployment(&models.Deployment{

+ 2 - 2
api/server/handlers/environment/finalize_deployment.go

@@ -7,9 +7,9 @@ import (
 
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -35,7 +35,7 @@ func (c *FinalizeDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 2 - 2
api/server/handlers/environment/get_deployment.go

@@ -6,9 +6,9 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -35,7 +35,7 @@ func (c *GetDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 2 - 2
api/server/handlers/environment/list_deployments.go

@@ -6,9 +6,9 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -41,7 +41,7 @@ func (c *ListDeploymentsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 2 - 2
api/server/handlers/environment/update_deployment.go

@@ -5,9 +5,9 @@ import (
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -35,7 +35,7 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 2 - 2
api/server/handlers/environment/update_deployment_status.go

@@ -5,9 +5,9 @@ import (
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -35,7 +35,7 @@ func (c *UpdateDeploymentStatusHandler) ServeHTTP(w http.ResponseWriter, r *http
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 3 - 2
api/server/handlers/gitinstallation/get_buildpack.go

@@ -11,6 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/integrations/buildpacks"
@@ -58,13 +59,13 @@ func (c *GithubGetBuildpackHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		return
 	}
 
-	owner, name, ok := GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return
 	}
 
-	branch, ok := GetBranch(c, w, r)
+	branch, ok := commonutils.GetBranchParam(c, w, r)
 
 	if !ok {
 		return

+ 3 - 2
api/server/handlers/gitinstallation/get_contents.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
@@ -37,13 +38,13 @@ func (c *GithubGetContentsHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	owner, name, ok := GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return
 	}
 
-	branch, ok := GetBranch(c, w, r)
+	branch, ok := commonutils.GetBranchParam(c, w, r)
 
 	if !ok {
 		return

+ 3 - 2
api/server/handlers/gitinstallation/get_procfile.go

@@ -11,6 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
@@ -41,13 +42,13 @@ func (c *GithubGetProcfileHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	owner, name, ok := GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return
 	}
 
-	branch, ok := GetBranch(c, w, r)
+	branch, ok := commonutils.GetBranchParam(c, w, r)
 
 	if !ok {
 		return

+ 3 - 2
api/server/handlers/gitinstallation/get_tarball_url.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
@@ -29,13 +30,13 @@ func NewGithubGetTarballURLHandler(
 }
 
 func (c *GithubGetTarballURLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	owner, name, ok := GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return
 	}
 
-	branch, ok := GetBranch(c, w, r)
+	branch, ok := commonutils.GetBranchParam(c, w, r)
 
 	if !ok {
 		return

+ 0 - 42
api/server/handlers/gitinstallation/helpers.go

@@ -3,14 +3,10 @@ package gitinstallation
 import (
 	"context"
 	"net/http"
-	"net/url"
 
 	ghinstallation "github.com/bradleyfalzon/ghinstallation/v2"
 	"github.com/google/go-github/v41/github"
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
@@ -136,41 +132,3 @@ func permissionToString(permission *string) string {
 
 	return *permission
 }
-
-// GetOwnerAndNameParams gets the owner and name ref for the Github repo
-func GetOwnerAndNameParams(c handlers.PorterHandler, w http.ResponseWriter, r *http.Request) (string, string, bool) {
-	owner, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoOwner)
-
-	if reqErr != nil {
-		c.HandleAPIError(w, r, reqErr)
-		return "", "", false
-	}
-
-	name, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoName)
-
-	if reqErr != nil {
-		c.HandleAPIError(w, r, reqErr)
-		return "", "", false
-	}
-
-	return owner, name, true
-}
-
-// GetBranch gets the unencoded branch
-func GetBranch(c handlers.PorterHandler, w http.ResponseWriter, r *http.Request) (string, bool) {
-	branch, reqErr := requestutils.GetURLParamString(r, types.URLParamGitBranch)
-
-	if reqErr != nil {
-		c.HandleAPIError(w, r, reqErr)
-		return "", false
-	}
-
-	branch, err := url.QueryUnescape(branch)
-
-	if reqErr != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return "", false
-	}
-
-	return branch, true
-}

+ 2 - 1
api/server/handlers/gitinstallation/list_branches.go

@@ -10,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
@@ -29,7 +30,7 @@ func NewGithubListBranchesHandler(
 }
 
 func (c *GithubListBranchesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	owner, name, ok := GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 1 - 1
api/server/handlers/gitinstallation/rerun_workflow.go

@@ -28,7 +28,7 @@ func NewRerunWorkflowHandler(
 }
 
 func (c *RerunWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	owner, name, ok := GetOwnerAndNameParams(c, w, r)
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
 		return

+ 7 - 0
api/server/handlers/project_integration/create_gitlab.go

@@ -31,6 +31,13 @@ func NewCreateGitlabIntegration(
 func (p *CreateGitlabIntegration) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
+	metadata := p.Config().Metadata
+
+	if !metadata.Gitlab {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("gitlab integration endpoints are not enabled")))
+		return
+	}
+
 	request := &types.CreateGitlabRequest{}
 
 	if ok := p.DecodeAndValidate(w, r, request); !ok {

+ 9 - 68
api/server/handlers/project_integration/get_gitlab_repo_buildpack.go

@@ -13,13 +13,11 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/integrations/buildpacks"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/oauth"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/xanzy/go-gitlab"
-	"gorm.io/gorm"
 )
 
 type GetGitlabRepoBuildpackHandler struct {
@@ -39,6 +37,7 @@ func NewGetGitlabRepoBuildpackHandler(
 func (p *GetGitlabRepoBuildpackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
 
 	request := &types.GetBuildpackRequest{}
 
@@ -48,87 +47,29 @@ func (p *GetGitlabRepoBuildpackHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
-	integrationID, reqErr := requestutils.GetURLParamUint(r, "integration_id")
+	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
 
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	owner, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoOwner)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	name, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoName)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	branch, reqErr := requestutils.GetURLParamString(r, types.URLParamGitBranch)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	gi, err := p.Repo().GitlabIntegration().ReadGitlabIntegration(project.ID, integrationID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("no gitlab integration with ID: %d", integrationID), http.StatusNotFound))
-			return
-		}
-
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	if !ok {
 		return
 	}
 
-	giAppOAuth, err := p.Repo().GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(user.ID, project.ID, integrationID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
-		}
+	branch, ok := commonutils.GetBranchParam(p, w, r)
 
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	if !ok {
 		return
 	}
 
-	oauthInt, err := p.Repo().OAuthIntegration().ReadOAuthIntegration(project.ID, giAppOAuth.OAuthIntegrationID)
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
 
 	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
 		}
 
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	accessToken, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, commonutils.GetGitlabOAuthConf(
-		p.Config(), gi,
-	), oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(project.ID, giAppOAuth, p.Repo()))
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("invalid gitlab access token"),
-			http.StatusUnauthorized))
-		return
-	}
-
-	client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	dir, err := url.QueryUnescape(request.Dir)
 
 	if err != nil {

+ 9 - 68
api/server/handlers/project_integration/get_gitlab_repo_contents.go

@@ -12,12 +12,10 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/oauth"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/xanzy/go-gitlab"
-	"gorm.io/gorm"
 )
 
 type GetGitlabRepoContentsHandler struct {
@@ -37,6 +35,7 @@ func NewGetGitlabRepoContentsHandler(
 func (p *GetGitlabRepoContentsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
 
 	request := &types.GetContentsRequest{}
 
@@ -46,31 +45,15 @@ func (p *GetGitlabRepoContentsHandler) ServeHTTP(w http.ResponseWriter, r *http.
 		return
 	}
 
-	integrationID, reqErr := requestutils.GetURLParamUint(r, "integration_id")
+	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
 
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	owner, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoOwner)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	name, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoName)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
+	if !ok {
 		return
 	}
 
-	branch, reqErr := requestutils.GetURLParamString(r, types.URLParamGitBranch)
+	branch, ok := commonutils.GetBranchParam(p, w, r)
 
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
+	if !ok {
 		return
 	}
 
@@ -87,59 +70,17 @@ func (p *GetGitlabRepoContentsHandler) ServeHTTP(w http.ResponseWriter, r *http.
 		dir = "."
 	}
 
-	gi, err := p.Repo().GitlabIntegration().ReadGitlabIntegration(project.ID, integrationID)
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
 
 	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("no gitlab integration with ID: %d", integrationID), http.StatusNotFound))
-			return
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
 		}
 
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	giAppOAuth, err := p.Repo().GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(user.ID, project.ID, integrationID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
-		}
-
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	oauthInt, err := p.Repo().OAuthIntegration().ReadOAuthIntegration(project.ID, giAppOAuth.OAuthIntegrationID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
-		}
-
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	accessToken, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, commonutils.GetGitlabOAuthConf(
-		p.Config(), gi,
-	), oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(project.ID, giAppOAuth, p.Repo()))
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("invalid gitlab access token"),
-			http.StatusUnauthorized))
-		return
-	}
-
-	client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	tree, resp, err := client.Repositories.ListTree(fmt.Sprintf("%s/%s", owner, name), &gitlab.ListTreeOptions{
 		Path: gitlab.String(dir),
 		Ref:  gitlab.String(branch),

+ 9 - 68
api/server/handlers/project_integration/get_gitlab_repo_procfile.go

@@ -13,12 +13,10 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/oauth"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/xanzy/go-gitlab"
-	"gorm.io/gorm"
 )
 
 var procfileRegex = regexp.MustCompile("^([A-Za-z0-9_]+):\\s*(.+)$")
@@ -40,6 +38,7 @@ func NewGetGitlabRepoProcfileHandler(
 func (p *GetGitlabRepoProcfileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
 
 	request := &types.GetProcfileRequest{}
 
@@ -49,31 +48,15 @@ func (p *GetGitlabRepoProcfileHandler) ServeHTTP(w http.ResponseWriter, r *http.
 		return
 	}
 
-	integrationID, reqErr := requestutils.GetURLParamUint(r, "integration_id")
+	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
 
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	owner, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoOwner)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	name, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoName)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
+	if !ok {
 		return
 	}
 
-	branch, reqErr := requestutils.GetURLParamString(r, types.URLParamGitBranch)
+	branch, ok := commonutils.GetBranchParam(p, w, r)
 
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
+	if !ok {
 		return
 	}
 
@@ -84,59 +67,17 @@ func (p *GetGitlabRepoProcfileHandler) ServeHTTP(w http.ResponseWriter, r *http.
 		return
 	}
 
-	gi, err := p.Repo().GitlabIntegration().ReadGitlabIntegration(project.ID, integrationID)
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
 
 	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("no gitlab integration with ID: %d", integrationID), http.StatusNotFound))
-			return
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
 		}
 
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	giAppOAuth, err := p.Repo().GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(user.ID, project.ID, integrationID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
-		}
-
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	oauthInt, err := p.Repo().OAuthIntegration().ReadOAuthIntegration(project.ID, giAppOAuth.OAuthIntegrationID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
-		}
-
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	accessToken, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, commonutils.GetGitlabOAuthConf(
-		p.Config(), gi,
-	), oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(project.ID, giAppOAuth, p.Repo()))
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("invalid gitlab access token"),
-			http.StatusUnauthorized))
-		return
-	}
-
-	client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	file, resp, err := client.RepositoryFiles.GetRawFile(fmt.Sprintf("%s/%s", owner, name),
 		strings.TrimPrefix(path, "./"), &gitlab.GetRawFileOptions{
 			Ref: gitlab.String(branch),

+ 7 - 65
api/server/handlers/project_integration/list_gitlab_repo_branches.go

@@ -10,12 +10,10 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/oauth"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/xanzy/go-gitlab"
-	"gorm.io/gorm"
 )
 
 type ListGitlabRepoBranchesHandler struct {
@@ -35,81 +33,25 @@ func NewListGitlabRepoBranchesHandler(
 func (p *ListGitlabRepoBranchesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
 
-	integrationID, reqErr := requestutils.GetURLParamUint(r, "integration_id")
+	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
 
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
+	if !ok {
 		return
 	}
 
-	owner, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoOwner)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	name, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoName)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	gi, err := p.Repo().GitlabIntegration().ReadGitlabIntegration(project.ID, integrationID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("no gitlab integration with ID: %d", integrationID), http.StatusNotFound))
-			return
-		}
-
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	giAppOAuth, err := p.Repo().GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(user.ID, project.ID, integrationID)
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
 
 	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
 		}
 
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	oauthInt, err := p.Repo().OAuthIntegration().ReadOAuthIntegration(project.ID, giAppOAuth.OAuthIntegrationID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
-		}
-
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	accessToken, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, commonutils.GetGitlabOAuthConf(
-		p.Config(), gi,
-	), oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(project.ID, giAppOAuth, p.Repo()))
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("invalid gitlab access token"),
-			http.StatusUnauthorized))
-		return
-	}
-
-	client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	branches, resp, err := client.Branches.ListBranches(fmt.Sprintf("%s/%s", owner, name), &gitlab.ListBranchesOptions{})
 
 	if resp.StatusCode == http.StatusUnauthorized {

+ 47 - 49
api/server/handlers/project_integration/list_gitlab_repos.go

@@ -10,14 +10,17 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/oauth"
+	"github.com/porter-dev/porter/internal/repository"
 	"github.com/xanzy/go-gitlab"
 	"gorm.io/gorm"
 )
 
+var errUnauthorizedGitlabUser = errors.New("unauthorized gitlab user")
+
 type ListGitlabReposHandler struct {
 	handlers.PorterHandlerReadWriter
 }
@@ -35,87 +38,82 @@ func NewListGitlabReposHandler(
 func (p *ListGitlabReposHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
+
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
 
-	integrationID, reqErr := requestutils.GetURLParamUint(r, "integration_id")
+	if err != nil {
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
+		}
 
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	gi, err := p.Repo().GitlabIntegration().ReadGitlabIntegration(project.ID, integrationID)
+	giProjects, resp, err := client.Projects.ListProjects(&gitlab.ListProjectsOptions{
+		Simple:     gitlab.Bool(true),
+		Membership: gitlab.Bool(true),
+	})
 
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("no gitlab integration with ID: %d", integrationID), http.StatusNotFound))
-			return
-		}
+	if resp.StatusCode == http.StatusUnauthorized {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
+		return
+	}
 
+	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	giAppOAuth, err := p.Repo().GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(user.ID, project.ID, integrationID)
+	var res []string
+
+	for _, giProject := range giProjects {
+		res = append(res, giProject.PathWithNamespace)
+	}
+
+	p.WriteResult(w, r, res)
+}
+
+func getGitlabClient(
+	repo repository.Repository,
+	userID, projectID uint,
+	gi *ints.GitlabIntegration,
+	config *config.Config,
+) (*gitlab.Client, error) {
+	giAppOAuth, err := repo.GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(userID, projectID, gi.ID)
 
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
+			return nil, errUnauthorizedGitlabUser
 		}
 
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		return nil, err
 	}
 
-	oauthInt, err := p.Repo().OAuthIntegration().ReadOAuthIntegration(project.ID, giAppOAuth.OAuthIntegrationID)
+	oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(projectID, giAppOAuth.OAuthIntegrationID)
 
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-			return
+			return nil, errUnauthorizedGitlabUser
 		}
 
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		return nil, err
 	}
 
 	accessToken, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, commonutils.GetGitlabOAuthConf(
-		p.Config(), gi,
-	), oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(project.ID, giAppOAuth, p.Repo()))
+		config, gi,
+	), oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(projectID, giAppOAuth, repo))
 
 	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("invalid gitlab access token"),
-			http.StatusUnauthorized))
-		return
+		return nil, errUnauthorizedGitlabUser
 	}
 
 	client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
 
 	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	giProjects, resp, err := client.Projects.ListProjects(&gitlab.ListProjectsOptions{
-		Simple:     gitlab.Bool(true),
-		Membership: gitlab.Bool(true),
-	})
-
-	if resp.StatusCode == http.StatusUnauthorized {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
-		return
-	}
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		return nil, err
 	}
 
-	var res []string
-
-	for _, giProject := range giProjects {
-		res = append(res, giProject.PathWithNamespace)
-	}
-
-	p.WriteResult(w, r, res)
+	return client, nil
 }

+ 18 - 9
api/server/router/project_integration.go

@@ -424,11 +424,12 @@ func getProjectIntegrationRoutes(
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/gitlab/{integration_id}/repos",
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos", relPath, types.URLParamIntegrationID),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
+				types.GitlabIntegrationScope,
 			},
 		},
 	)
@@ -451,12 +452,14 @@ func getProjectIntegrationRoutes(
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/gitlab/{integration_id}/repos/{%s}/{%s}/branches", relPath, types.URLParamGitRepoOwner, types.URLParamGitRepoName),
+				Parent: basePath,
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/{%s}/{%s}/branches",
+					relPath, types.URLParamIntegrationID, types.URLParamGitRepoOwner, types.URLParamGitRepoName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
+				types.GitlabIntegrationScope,
 			},
 		},
 	)
@@ -480,12 +483,14 @@ func getProjectIntegrationRoutes(
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent: basePath,
-				RelativePath: fmt.Sprintf("%s/gitlab/{integration_id}/repos/{%s}/{%s}/{%s}/contents", relPath,
-					types.URLParamGitRepoOwner, types.URLParamGitRepoName, types.URLParamGitBranch),
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/{%s}/{%s}/{%s}/contents", relPath,
+					types.URLParamIntegrationID, types.URLParamGitRepoOwner,
+					types.URLParamGitRepoName, types.URLParamGitBranch),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
+				types.GitlabIntegrationScope,
 			},
 		},
 	)
@@ -509,12 +514,14 @@ func getProjectIntegrationRoutes(
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent: basePath,
-				RelativePath: fmt.Sprintf("%s/gitlab/{integration_id}/repos/{%s}/{%s}/{%s}/buildpack/detect", relPath,
-					types.URLParamGitRepoOwner, types.URLParamGitRepoName, types.URLParamGitBranch),
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/{%s}/{%s}/{%s}/buildpack/detect", relPath,
+					types.URLParamIntegrationID, types.URLParamGitRepoOwner,
+					types.URLParamGitRepoName, types.URLParamGitBranch),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
+				types.GitlabIntegrationScope,
 			},
 		},
 	)
@@ -538,12 +545,14 @@ func getProjectIntegrationRoutes(
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent: basePath,
-				RelativePath: fmt.Sprintf("%s/gitlab/{integration_id}/repos/{%s}/{%s}/{%s}/procfile", relPath,
-					types.URLParamGitRepoOwner, types.URLParamGitRepoName, types.URLParamGitBranch),
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/{%s}/{%s}/{%s}/procfile", relPath,
+					types.URLParamIntegrationID, types.URLParamGitRepoOwner,
+					types.URLParamGitRepoName, types.URLParamGitBranch),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
+				types.GitlabIntegrationScope,
 			},
 		},
 	)

+ 5 - 0
api/server/router/router.go

@@ -217,6 +217,9 @@ func registerRoutes(config *config.Config, routes []*router.Route) {
 	// websocket middleware for upgrading requests
 	websocketMw := middleware.NewWebsocketMiddleware(config)
 
+	// gitlab integration middleware to handle gitlab integrations for a specific project
+	gitlabIntFactory := authz.NewGitlabIntegrationScopedFactory(config)
+
 	for _, route := range routes {
 		atomicGroup := route.Router.Group(nil)
 
@@ -252,6 +255,8 @@ func registerRoutes(config *config.Config, routes []*router.Route) {
 				atomicGroup.Use(operationFactory.Middleware)
 			case types.ReleaseScope:
 				atomicGroup.Use(releaseFactory.Middleware)
+			case types.GitlabIntegrationScope:
+				atomicGroup.Use(gitlabIntFactory.Middleware)
 			}
 		}
 

+ 43 - 0
api/server/shared/commonutils/git_utils.go

@@ -4,9 +4,14 @@ import (
 	"context"
 	"errors"
 	"net/http"
+	"net/url"
 	"time"
 
 	"github.com/google/go-github/v41/github"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
 )
 
 var ErrNoWorkflowRuns = errors.New("no previous workflow runs found")
@@ -40,3 +45,41 @@ func GetLatestWorkflowRun(client *github.Client, owner, repo, filename, branch s
 
 	return workflowRuns.WorkflowRuns[0], nil
 }
+
+// GetOwnerAndNameParams gets the owner and name ref for the git repo
+func GetOwnerAndNameParams(c handlers.PorterHandler, w http.ResponseWriter, r *http.Request) (string, string, bool) {
+	owner, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoOwner)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return "", "", false
+	}
+
+	name, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoName)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return "", "", false
+	}
+
+	return owner, name, true
+}
+
+// GetBranchParam gets the unencoded branch for the git repo
+func GetBranchParam(c handlers.PorterHandler, w http.ResponseWriter, r *http.Request) (string, bool) {
+	branch, reqErr := requestutils.GetURLParamString(r, types.URLParamGitBranch)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return "", false
+	}
+
+	branch, err := url.QueryUnescape(branch)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return "", false
+	}
+
+	return branch, true
+}

+ 3 - 0
api/server/shared/config/env/envconfs.go

@@ -99,6 +99,9 @@ type ServerConf struct {
 
 	// Disable filtering for project creation
 	DisableAllowlist bool `env:"DISABLE_ALLOWLIST,default=false"`
+
+	// Enable gitlab integration
+	EnableGitlab bool `env:"ENABLE_GITLAB,default=false"`
 }
 
 // DBConf is the database configuration: if generated from environment variables,

+ 2 - 0
api/server/shared/config/metadata.go

@@ -14,6 +14,7 @@ type Metadata struct {
 	Email              bool   `json:"email"`
 	Analytics          bool   `json:"analytics"`
 	Version            string `json:"version"`
+	Gitlab             bool   `json:"gitlab"`
 }
 
 func MetadataFromConf(sc *env.ServerConf, version string) *Metadata {
@@ -27,6 +28,7 @@ func MetadataFromConf(sc *env.ServerConf, version string) *Metadata {
 		Email:              sc.SendgridAPIKey != "",
 		Analytics:          sc.SegmentClientKey != "",
 		Version:            version,
+		Gitlab:             sc.EnableGitlab,
 	}
 }
 

+ 13 - 12
api/types/policy.go

@@ -5,18 +5,19 @@ import "time"
 type PermissionScope string
 
 const (
-	UserScope            PermissionScope = "user"
-	ProjectScope         PermissionScope = "project"
-	ClusterScope         PermissionScope = "cluster"
-	RegistryScope        PermissionScope = "registry"
-	InviteScope          PermissionScope = "invite"
-	HelmRepoScope        PermissionScope = "helm_repo"
-	InfraScope           PermissionScope = "infra"
-	OperationScope       PermissionScope = "operation"
-	GitInstallationScope PermissionScope = "git_installation"
-	NamespaceScope       PermissionScope = "namespace"
-	SettingsScope        PermissionScope = "settings"
-	ReleaseScope         PermissionScope = "release"
+	UserScope              PermissionScope = "user"
+	ProjectScope           PermissionScope = "project"
+	ClusterScope           PermissionScope = "cluster"
+	RegistryScope          PermissionScope = "registry"
+	InviteScope            PermissionScope = "invite"
+	HelmRepoScope          PermissionScope = "helm_repo"
+	InfraScope             PermissionScope = "infra"
+	OperationScope         PermissionScope = "operation"
+	GitInstallationScope   PermissionScope = "git_installation"
+	NamespaceScope         PermissionScope = "namespace"
+	SettingsScope          PermissionScope = "settings"
+	ReleaseScope           PermissionScope = "release"
+	GitlabIntegrationScope PermissionScope = "gitlab_integration"
 )
 
 type NameOrUInt struct {

+ 1 - 0
api/types/request.go

@@ -45,6 +45,7 @@ const (
 	URLParamReleaseName       URLParam = "name"
 	URLParamReleaseVersion    URLParam = "version"
 	URLParamWildcard          URLParam = "*"
+	URLParamIntegrationID     URLParam = "integration_id"
 )
 
 type Path struct {

+ 15 - 0
cli/cmd/config.go

@@ -154,6 +154,20 @@ var configSetHostCmd = &cobra.Command{
 	},
 }
 
+var configSetKubeconfigCmd = &cobra.Command{
+	Use:   "set-kubeconfig [kubeconfig-path]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Saves the path to kubeconfig in the default configuration",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := cliConf.SetKubeconfig(args[0])
+
+		if err != nil {
+			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			os.Exit(1)
+		}
+	},
+}
+
 func init() {
 	rootCmd.AddCommand(configCmd)
 
@@ -162,6 +176,7 @@ func init() {
 	configCmd.AddCommand(configSetHostCmd)
 	configCmd.AddCommand(configSetRegistryCmd)
 	configCmd.AddCommand(configSetHelmRepoCmd)
+	configCmd.AddCommand(configSetKubeconfigCmd)
 }
 
 func printConfig() error {

+ 41 - 2
cli/cmd/config/config.go

@@ -1,6 +1,8 @@
 package config
 
 import (
+	"errors"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -32,8 +34,9 @@ type CLIConfig struct {
 
 	Token string `yaml:"token"`
 
-	Registry uint `yaml:"registry"`
-	HelmRepo uint `yaml:"helm_repo"`
+	Registry   uint   `yaml:"registry"`
+	HelmRepo   uint   `yaml:"helm_repo"`
+	Kubeconfig string `yaml:"kubeconfig"`
 }
 
 // InitAndLoadConfig populates the config object with the following precedence rules:
@@ -209,6 +212,12 @@ func (c *CLIConfig) SetHost(host string) error {
 }
 
 func (c *CLIConfig) SetProject(projectID uint) error {
+	if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
+		viper.Set("kubeconfig", "")
+		color.New(color.FgBlue).Println("Removing local kubeconfig")
+		config.Kubeconfig = ""
+	}
+
 	viper.Set("project", projectID)
 	color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)
 	err := viper.WriteConfig()
@@ -223,6 +232,12 @@ func (c *CLIConfig) SetProject(projectID uint) error {
 }
 
 func (c *CLIConfig) SetCluster(clusterID uint) error {
+	if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
+		viper.Set("kubeconfig", "")
+		color.New(color.FgBlue).Println("Removing local kubeconfig")
+		config.Kubeconfig = ""
+	}
+
 	viper.Set("cluster", clusterID)
 	color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)
 	err := viper.WriteConfig()
@@ -276,3 +291,27 @@ func (c *CLIConfig) SetHelmRepo(helmRepoID uint) error {
 
 	return nil
 }
+
+func (c *CLIConfig) SetKubeconfig(kubeconfig string) error {
+	path, err := filepath.Abs(kubeconfig)
+
+	if err != nil {
+		return err
+	}
+
+	if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
+		return fmt.Errorf("%s does not exist", path)
+	}
+
+	viper.Set("kubeconfig", path)
+	color.New(color.FgGreen).Printf("Set the path to kubeconfig as %s\n", path)
+	err = viper.WriteConfig()
+
+	if err != nil {
+		return err
+	}
+
+	config.Kubeconfig = kubeconfig
+
+	return nil
+}

+ 1 - 1
cli/cmd/portforward.go

@@ -145,7 +145,7 @@ func portForward(user *types.GetAuthenticatedUserResponse, client *api.Client, a
 		pod = pods[0]
 	}
 
-	kubeResp, err := client.GetKubeconfig(context.Background(), cliConf.Project, cliConf.Cluster)
+	kubeResp, err := client.GetKubeconfig(context.Background(), cliConf.Project, cliConf.Cluster, cliConf.Kubeconfig)
 
 	if err != nil {
 		return err

+ 1 - 1
cli/cmd/run.go

@@ -264,7 +264,7 @@ func (p *PorterRunSharedConfig) setSharedConfig() error {
 	pID := cliConf.Project
 	cID := cliConf.Cluster
 
-	kubeResp, err := p.Client.GetKubeconfig(context.TODO(), pID, cID)
+	kubeResp, err := p.Client.GetKubeconfig(context.Background(), pID, cID, cliConf.Kubeconfig)
 
 	if err != nil {
 		return err

+ 24 - 0
dashboard/package-lock.json

@@ -1670,6 +1670,30 @@
         "@types/node": "*"
       }
     },
+    "@types/color": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
+      "integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==",
+      "dev": true,
+      "requires": {
+        "@types/color-convert": "*"
+      }
+    },
+    "@types/color-convert": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz",
+      "integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==",
+      "dev": true,
+      "requires": {
+        "@types/color-name": "*"
+      }
+    },
+    "@types/color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
+      "dev": true
+    },
     "@types/connect": {
       "version": "3.4.35",
       "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",

+ 104 - 48
dashboard/src/components/repo-selector/RepoList.tsx

@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useRef, useState } from "react";
 import styled from "styled-components";
 import github from "assets/github.png";
 
@@ -9,6 +9,7 @@ import { Context } from "shared/Context";
 import Loading from "../Loading";
 import SearchBar from "../SearchBar";
 import DynamicLink from "components/DynamicLink";
+import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
 
 type Props = {
   actionConfig: ActionConfigType | null;
@@ -173,13 +174,13 @@ const RepoList: React.FC<Props> = ({
       if (currentProvider.provider === "gitlab") {
         return (
           <LoadingWrapper>
-            We couldn't reach gitlab to get your repos. You can
+            GitLab could not be reached.
             <A
               to={`${window.location.origin}/api/projects/${currentProject.id}/oauth/gitlab?integration_id=${currentProvider.integration_id}`}
             >
-              Connect your gitlab account to porter
+              Connect your GitLab account to Porter
             </A>
-            or select another git provider.
+            or select another Git provider.
           </LoadingWrapper>
         );
       } else {
@@ -242,7 +243,7 @@ const RepoList: React.FC<Props> = ({
     } else {
       return (
         <>
-          <div style={{ display: "flex" }}>
+          <div style={{ display: "flex", marginBottom: "10px" }}>
             <ProviderSelector
               values={providers}
               currentValue={currentProvider}
@@ -251,7 +252,7 @@ const RepoList: React.FC<Props> = ({
             <SearchBar
               setSearchFilter={setSearchFilter}
               disabled={repoError || repoLoading}
-              prompt={"Search repos..."}
+              prompt={"Search repos . . ."}
               fullWidth
             />
           </div>
@@ -277,18 +278,16 @@ const RepoList: React.FC<Props> = ({
                   justifyContent: "center",
                 }}
               >
-                <div>We couldn't find any git provider to connect with.</div>
+                <div>A connected Git provider wasn't found.</div>
                 <div>
                   You can
                   <A
                     to={`${window.location.origin}/api/integrations/github-app/install`}
                   >
-                    Add Github repositories
+                    connect a GitHub repo
                   </A>
                   or
-                  <A to={"/integrations"}>
-                    Add a Gitlab instance to your project
-                  </A>
+                  <A to={"/integrations"}>add a GitLab instance</A>
                 </div>
               </div>
             </LoadingWrapper>
@@ -308,73 +307,130 @@ const ProviderSelector = (props: {
   currentValue: any;
   onChange: (provider: any) => void;
 }) => {
+  const wrapperRef = useRef();
   const { values, currentValue, onChange } = props;
   const [isOpen, setIsOpen] = useState(false);
   const icon = `devicon-${currentValue?.provider}-plain colored`;
+  useOutsideAlerter(wrapperRef, () => {
+    setIsOpen(false);
+  });
+
+  if (!currentValue) {
+    return (
+      <ProviderSelectorStyles.Wrapper>
+        <Loading />
+      </ProviderSelectorStyles.Wrapper>
+    );
+  }
+
   return (
-    <ProviderSelectorStyles.Wrapper>
-      <ProviderSelectorStyles.Button onClick={() => setIsOpen((prev) => !prev)}>
+    <>
+      <ProviderSelectorStyles.Wrapper ref={wrapperRef} isOpen={isOpen}>
         <ProviderSelectorStyles.Icon className={icon} />
-        {currentValue?.name || currentValue?.instance_url}
-      </ProviderSelectorStyles.Button>
-      {isOpen ? (
-        <ProviderSelectorStyles.OptionWrapper>
-          {values.map((provider) => {
-            return (
-              <ProviderSelectorStyles.Option onClick={() => onChange(provider)}>
-                <ProviderSelectorStyles.Icon
-                  className={`devicon-${provider?.provider}-plain colored`}
-                />
-                {provider?.name || provider?.instance_url}
-              </ProviderSelectorStyles.Option>
-            );
-          })}
-        </ProviderSelectorStyles.OptionWrapper>
-      ) : null}
-    </ProviderSelectorStyles.Wrapper>
+
+        <ProviderSelectorStyles.Button
+          onClick={() => setIsOpen((prev) => !prev)}
+        >
+          {currentValue?.name || currentValue?.instance_url}
+        </ProviderSelectorStyles.Button>
+        <i className="material-icons">arrow_drop_down</i>
+        {isOpen ? (
+          <>
+            <ProviderSelectorStyles.OptionWrapper>
+              {values.map((provider) => {
+                return (
+                  <ProviderSelectorStyles.Option
+                    onClick={() => {
+                      setIsOpen(false);
+                      onChange(provider);
+                    }}
+                  >
+                    <ProviderSelectorStyles.Icon
+                      className={`devicon-${provider?.provider}-plain colored`}
+                    />
+                    <ProviderSelectorStyles.Text>
+                      {provider?.name || provider?.instance_url}
+                    </ProviderSelectorStyles.Text>
+                  </ProviderSelectorStyles.Option>
+                );
+              })}
+            </ProviderSelectorStyles.OptionWrapper>
+          </>
+        ) : null}
+      </ProviderSelectorStyles.Wrapper>
+    </>
   );
 };
 
 const ProviderSelectorStyles = {
-  Wrapper: styled.div`
+  Wrapper: styled.div<{ isOpen?: boolean }>`
     position: relative;
     margin-bottom: 10px;
-    margin-right: 5px;
+    height: 40px;
+    display: flex;
+    min-width: 50%;
+    cursor: pointer;
+    margin-right: 10px;
+    margin-left: 2px;
+    align-items: center;
+
+    > i {
+      margin-left: -26px;
+      margin-right: 10px;
+      z-index: 0;
+      transform: ${(props) => (props.isOpen ? "rotate(180deg)" : "")};
+    }
   `,
   Button: styled.div`
-    border-radius: 5px;
-    border: 1px solid white;
     height: 100%;
+    font-weight: bold;
+    font-size: 14px;
     border-bottom: 0;
-    border: 1px solid #ffffff55;
-    border-radius: 3px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
+    z-index: 999;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
     padding: 6px 15px;
+    padding-left: 40px;
+    padding-right: 28px;
+    border-bottom: 2px solid #ffffff;
+    padding-top: 11px;
   `,
   OptionWrapper: styled.div`
+    top: 40px;
     position: absolute;
-    background-color: #202227;
+    background: #37393f;
+    border-radius: 3px;
+    width: calc(100% - 4px);
+    box-shadow: 0 8px 20px 0px #00000088;
   `,
   Option: styled.div`
-    white-space: nowrap;
-    padding: 8px 10px;
     display: flex;
     align-items: center;
-    :not(:last-child) {
-      border-bottom: 1px solid black;
-      border-bottom-width: 90%;
-    }
+
     :hover {
-      background-color: #363940;
+      background-color: #ffffff22;
     }
   `,
   Icon: styled.span`
     font-size: 24px;
-    margin-right: 15px;
+    margin-left: 9px;
+    margin-right: -29px;
     color: white;
   `,
+  Text: styled.div`
+    font-weight: bold;
+    font-size: 14px;
+    margin-left: 40px;
+    height: 45px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    padding: 8px 10px;
+    width: 100%;
+    padding-top: 14px;
+    padding-left: 0;
+  `,
 };
 
 const RepoListWrapper = styled.div`

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

@@ -67,6 +67,7 @@ export default class Main extends Component<PropsType, StateType> {
       .then((res) => {
         this.context.setEdition(res.data?.version);
         this.setState({ local: !res.data?.provisioner });
+        this.context.setEnableGitlab(res.data?.gitlab ? true : false);
       })
       .catch((err) => console.log(err));
   }

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

@@ -42,7 +42,7 @@ const GitlabIntegrationList: React.FC<Props> = (props) => {
             );
           })
         ) : (
-          <Placeholder>No GitLab instances setted up yet.</Placeholder>
+          <Placeholder>No GitLab instances found</Placeholder>
         )}
       </StyledIntegrationList>
     </>

+ 12 - 7
dashboard/src/main/home/integrations/Integrations.tsx

@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useContext, useMemo } from "react";
 import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
 
 import { integrationList } from "shared/common";
@@ -9,16 +9,21 @@ import CreateIntegrationForm from "./create-integration/CreateIntegrationForm";
 import IntegrationCategories from "./IntegrationCategories";
 import IntegrationList from "./IntegrationList";
 import TitleSection from "components/TitleSection";
+import { Context } from "shared/Context";
 
 type PropsType = RouteComponentProps;
 
-const IntegrationCategoryStrings = [
-  "registry",
-  "slack",
-  ...(process.env.ENABLE_GITLAB ? ["gitlab"] : []),
-]; /*"kubernetes",*/
-
 const Integrations: React.FC<PropsType> = (props) => {
+  const { enableGitlab } = useContext(Context);
+
+  const IntegrationCategoryStrings = useMemo(() => {
+    if (!enableGitlab) {
+      return ["registry", "slack"];
+    }
+
+    return ["registry", "slack", "gitlab"];
+  }, [enableGitlab]);
+
   return (
     <StyledIntegrations>
       <Switch>

+ 5 - 3
dashboard/src/main/home/integrations/create-integration/GitlabForm.tsx

@@ -91,7 +91,7 @@ const GitlabForm: React.FC<Props> = () => {
     <>
       <StyledForm>
         <CredentialWrapper>
-          <Heading>Gitlab instance settings</Heading>
+          <Heading>GitLab Instance Settings</Heading>
 
           <InputRow
             type="string"
@@ -104,7 +104,7 @@ const GitlabForm: React.FC<Props> = () => {
           />
           <InputRow
             type="string"
-            label="Client ID"
+            label="Client Application ID"
             value={clientId}
             setValue={(val: string) => setClientId(val)}
             isRequired
@@ -123,8 +123,10 @@ const GitlabForm: React.FC<Props> = () => {
         </CredentialWrapper>
         <SaveButton
           onClick={submit}
-          text="Save new Gitlab instance"
+          makeFlush={true}
+          text="Save Gitlab Settings"
           status={buttonStatus || error?.message}
+          
         />
       </StyledForm>
     </>

+ 2 - 2
dashboard/src/main/home/launch/launch-flow/SourcePage.tsx

@@ -65,7 +65,7 @@ class SourcePage extends Component<PropsType, StateType> {
     if (sourceType === "") {
       return (
         <BlockList>
-          {capabilities.github && (
+          {capabilities.github || capabilities.gitlab ? (
             <Block onClick={() => setSourceType("repo")}>
               <BlockIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
               <BlockTitle>Git Repository</BlockTitle>
@@ -73,7 +73,7 @@ class SourcePage extends Component<PropsType, StateType> {
                 Deploy using source from a Git repo.
               </BlockDescription>
             </Block>
-          )}
+          ) : null}
           <Block onClick={() => setSourceType("registry")}>
             <BlockIcon src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png" />
             <BlockTitle>Docker Registry</BlockTitle>

+ 27 - 2
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -43,7 +43,7 @@ class ProjectSettings extends Component<PropsType, StateType> {
       !this.state.tabOptions.find((t) => t.value === "billing")
     ) {
       const tabOptions = this.state.tabOptions;
-      tabOptions.splice(1, 0, { value: "billing", label: "Billing" });
+      // tabOptions.splice(1, 0, { value: "billing", label: "Billing" });
       this.setState({ tabOptions });
       return;
     }
@@ -56,7 +56,7 @@ class ProjectSettings extends Component<PropsType, StateType> {
       const billingIndex = this.state.tabOptions.findIndex(
         (t) => t.value === "billing"
       );
-      tabOptions.splice(billingIndex, 1);
+      // tabOptions.splice(billingIndex, 1);
     }
   }
 
@@ -65,6 +65,10 @@ 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",
+    });
 
     if (this.props.isAuthorized("settings", "", ["get", "delete"])) {
       if (this.context?.hasBillingEnabled) {
@@ -111,6 +115,14 @@ class ProjectSettings extends Component<PropsType, StateType> {
       return <InvitePage />;
     } else if (this.state.currentTab === "api-tokens") {
       return <APITokensSection />;
+    } else if (this.state.currentTab === "billing") {
+      return (
+        <Placeholder>
+          <Helper>
+          Please contact <a href="mailto:support@porter.run">support@porter.run</a> to upgrade your project's usage limits.
+          </Helper>
+        </Placeholder>
+      );
     } else {
       return (
         <>
@@ -173,6 +185,19 @@ ProjectSettings.contextType = Context;
 
 export default withRouter(withAuth(ProjectSettings));
 
+const Placeholder = styled.div`
+  width: 100%;
+  height: 200px;
+  background: #ffffff11;
+  border-radius: 3px;
+  display: flex;
+  align-items: center;
+  text-align: center;
+  padding: 0 30px;
+  justify-content: center;
+  padding-bottom: 10px;
+`;
+
 const Warning = styled.div`
   font-size: 13px;
   color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>

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

@@ -62,6 +62,8 @@ export interface GlobalContextType {
   setHasFinishedOnboarding: (onboardingStatus: boolean) => void;
   canCreateProject: boolean;
   setCanCreateProject: (canCreateProject: boolean) => void;
+  enableGitlab: boolean;
+  setEnableGitlab: (enableGitlab: boolean) => void;
 }
 
 /**
@@ -187,6 +189,10 @@ class ContextProvider extends Component<PropsType, StateType> {
     setCanCreateProject: (canCreateProject: boolean) => {
       this.setState({ canCreateProject });
     },
+    enableGitlab: false,
+    setEnableGitlab: (enableGitlab) => {
+      this.setState({ enableGitlab });
+    },
   };
 
   render() {

+ 2 - 2
dashboard/src/shared/common.tsx

@@ -107,8 +107,8 @@ export const integrationList: any = {
   },
   gitlab: {
     icon: "https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png",
-    label: "Gitlab",
-    buttonText: "Add instance",
+    label: "GitLab",
+    buttonText: "Add an Instance",
   },
   rds: {
     icon:

+ 30 - 0
dashboard/src/shared/hooks/useOutsideAlerter.ts

@@ -0,0 +1,30 @@
+import React, { MutableRefObject, RefObject, useEffect, useRef } from "react";
+
+/**
+ * Hook that alerts clicks outside of the passed ref
+ */
+export function useOutsideAlerter(
+  ref: React.RefObject<HTMLDivElement>,
+  callback: () => void
+) {
+  useEffect(() => {
+    /**
+     * Alert if clicked on outside of element
+     */
+    function handleClickOutside(event: any) {
+      if (
+        ref.current &&
+        !ref.current.contains(event.target) &&
+        typeof callback === "function"
+      ) {
+        callback();
+      }
+    }
+    // Bind the event listener
+    document.addEventListener("mousedown", handleClickOutside);
+    return () => {
+      // Unbind the event listener on clean up
+      document.removeEventListener("mousedown", handleClickOutside);
+    };
+  }, [ref]);
+}

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

@@ -346,6 +346,8 @@ export interface ContextProps {
   setHasFinishedOnboarding: (onboardingStatus: boolean) => void;
   canCreateProject: boolean;
   setCanCreateProject: (canCreateProject: boolean) => void;
+  enableGitlab: boolean;
+  setEnableGitlab: (enableGitlab: boolean) => void;
 }
 
 export enum JobStatusType {

+ 6 - 5
go.mod

@@ -45,7 +45,7 @@ require (
 	github.com/spf13/cobra v1.4.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.10.0
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.7.1
 	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
 	golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
 	golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
@@ -79,6 +79,7 @@ require (
 	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/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
+	github.com/cosmtrek/air v1.30.0 // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
@@ -131,7 +132,7 @@ require (
 	github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
 	github.com/evanphx/json-patch v4.12.0+incompatible // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
-	github.com/fsnotify/fsnotify v1.5.1 // 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
@@ -162,7 +163,7 @@ require (
 	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/imdario/mergo v0.3.12 // indirect
+	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3 // indirect
 	github.com/itchyny/timefmt-go v0.1.1 // indirect
@@ -249,7 +250,7 @@ require (
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
 	golang.org/x/mod v0.5.1 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
-	golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
+	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
 	golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
@@ -260,7 +261,7 @@ require (
 	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.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.0 // indirect
 	k8s.io/apiextensions-apiserver v0.23.1 // indirect
 	k8s.io/apiserver v0.23.1 // indirect
 	k8s.io/component-base v0.23.1 // indirect

+ 13 - 0
go.sum

@@ -457,6 +457,8 @@ github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cosmtrek/air v1.30.0 h1:Ge27Ye0ZXIcxslU+zy7WKPZAsSz+ws5L+BUGmIRvTLg=
+github.com/cosmtrek/air v1.30.0/go.mod h1:eNJLyoFnfPbr6F/h6FVFyL2Y0ia7A61ULjtk+EB5uaY=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -586,6 +588,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
+github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
+github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
 github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
 github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
@@ -942,6 +946,8 @@ github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -1553,6 +1559,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@@ -2037,8 +2045,11 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2391,6 +2402,8 @@ gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To=
 gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs=
 gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=

+ 1 - 1
internal/integrations/ci/actions/steps.go

@@ -55,7 +55,7 @@ func getCreatePreviewEnvStep(
 			"project": fmt.Sprintf("%d", projectID),
 			"token":   fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
 			"namespace": fmt.Sprintf("pr-${{ github.event.inputs.pr_number }}-%s",
-				strings.ReplaceAll(repoName, "_", "-")),
+				strings.ToLower(strings.ReplaceAll(repoName, "_", "-"))),
 			"pr_id":           "${{ github.event.inputs.pr_number }}",
 			"pr_name":         "${{ github.event.inputs.pr_title }}",
 			"installation_id": fmt.Sprintf("%d", gitInstallationID),