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

Merge pull request #2114 from porter-dev/staging

API tokens + v1 endpoints + hotfixes -> production
abelanger5 4 лет назад
Родитель
Сommit
71ce9ad66c
100 измененных файлов с 4409 добавлено и 634 удалено
  1. 62 0
      .github/workflows/dev.yaml
  2. 29 0
      api/client/k8s.go
  3. 38 7
      api/server/authn/handler.go
  4. 64 0
      api/server/authz/gitlab_integration.go
  5. 23 5
      api/server/authz/policy.go
  6. 107 58
      api/server/authz/policy/loader.go
  7. 18 9
      api/server/authz/policy/loader_test.go
  8. 11 1
      api/server/authz/policy/policy.go
  9. 3 3
      api/server/authz/policy/policy_test.go
  10. 4 4
      api/server/authz/policy_test.go
  11. 118 0
      api/server/handlers/api_token/create.go
  12. 72 0
      api/server/handlers/api_token/get.go
  13. 51 0
      api/server/handlers/api_token/list.go
  14. 73 0
      api/server/handlers/api_token/revoke.go
  15. 1 0
      api/server/handlers/cluster/create_namespace.go
  16. 11 4
      api/server/handlers/cluster/delete_namespace.go
  17. 61 0
      api/server/handlers/cluster/get_namespace.go
  18. 22 5
      api/server/handlers/environment/create.go
  19. 2 2
      api/server/handlers/environment/create_deployment.go
  20. 28 18
      api/server/handlers/environment/delete.go
  21. 2 2
      api/server/handlers/environment/finalize_deployment.go
  22. 2 2
      api/server/handlers/environment/get_deployment.go
  23. 2 2
      api/server/handlers/environment/list_deployments.go
  24. 2 2
      api/server/handlers/environment/update_deployment.go
  25. 2 2
      api/server/handlers/environment/update_deployment_status.go
  26. 4 3
      api/server/handlers/gitinstallation/get_buildpack.go
  27. 3 2
      api/server/handlers/gitinstallation/get_contents.go
  28. 3 2
      api/server/handlers/gitinstallation/get_procfile.go
  29. 3 2
      api/server/handlers/gitinstallation/get_tarball_url.go
  30. 0 42
      api/server/handlers/gitinstallation/helpers.go
  31. 2 1
      api/server/handlers/gitinstallation/list_branches.go
  32. 1 1
      api/server/handlers/gitinstallation/rerun_workflow.go
  33. 31 2
      api/server/handlers/handler.go
  34. 4 0
      api/server/handlers/infra/create.go
  35. 21 0
      api/server/handlers/infra/forms.go
  36. 2 0
      api/server/handlers/infra/get_template.go
  37. 8 0
      api/server/handlers/infra/list_templates.go
  38. 4 0
      api/server/handlers/namespace/list_releases.go
  39. 132 0
      api/server/handlers/oauth_callback/gitlab.go
  40. 89 0
      api/server/handlers/policy/create.go
  41. 66 0
      api/server/handlers/policy/get.go
  42. 45 0
      api/server/handlers/policy/list.go
  43. 5 2
      api/server/handlers/project/get_policy.go
  44. 73 0
      api/server/handlers/project_integration/create_gitlab.go
  45. 160 0
      api/server/handlers/project_integration/get_gitlab_repo_buildpack.go
  46. 112 0
      api/server/handlers/project_integration/get_gitlab_repo_contents.go
  47. 111 0
      api/server/handlers/project_integration/get_gitlab_repo_procfile.go
  48. 117 0
      api/server/handlers/project_integration/list_git.go
  49. 44 0
      api/server/handlers/project_integration/list_gitlab.go
  50. 77 0
      api/server/handlers/project_integration/list_gitlab_repo_branches.go
  51. 119 0
      api/server/handlers/project_integration/list_gitlab_repos.go
  52. 1 1
      api/server/handlers/project_oauth/digitalocean.go
  53. 78 0
      api/server/handlers/project_oauth/gitlab.go
  54. 1 1
      api/server/handlers/project_oauth/slack.go
  55. 206 67
      api/server/handlers/release/create.go
  56. 53 20
      api/server/handlers/release/delete.go
  57. 41 0
      api/server/handlers/release/get.go
  58. 1 1
      api/server/handlers/release/ugprade.go
  59. 1 1
      api/server/handlers/release/update_rollback.go
  60. 1 1
      api/server/handlers/user/github_start.go
  61. 1 1
      api/server/handlers/user/google_start.go
  62. 28 27
      api/server/router/base.go
  63. 46 45
      api/server/router/cluster.go
  64. 24 23
      api/server/router/git_installation.go
  65. 10 9
      api/server/router/helm_repo.go
  66. 20 19
      api/server/router/infra.go
  67. 12 11
      api/server/router/invite.go
  68. 28 27
      api/server/router/namespace.go
  69. 32 7
      api/server/router/oauth_callback.go
  70. 247 41
      api/server/router/project.go
  71. 258 16
      api/server/router/project_integration.go
  72. 37 8
      api/server/router/project_oauth.go
  73. 13 12
      api/server/router/registry.go
  74. 32 31
      api/server/router/release.go
  75. 44 22
      api/server/router/router.go
  76. 10 9
      api/server/router/slack_integration.go
  77. 23 22
      api/server/router/user.go
  78. 172 0
      api/server/router/v1/cluster.go
  79. 56 0
      api/server/router/v1/namespace.go
  80. 56 0
      api/server/router/v1/project.go
  81. 259 0
      api/server/router/v1/registry.go
  82. 212 0
      api/server/router/v1/release.go
  83. 25 0
      api/server/shared/apierrors/errors.go
  84. 43 0
      api/server/shared/commonutils/git_utils.go
  85. 20 0
      api/server/shared/commonutils/gitlab.go
  86. 3 0
      api/server/shared/config/env/envconfs.go
  87. 2 0
      api/server/shared/config/metadata.go
  88. 28 0
      api/server/shared/router/router.go
  89. 28 0
      api/types/api_token.go
  90. 12 8
      api/types/git_action_config.go
  91. 1 0
      api/types/infra.go
  92. 101 12
      api/types/policy.go
  93. 1 0
      api/types/project.go
  94. 34 0
      api/types/project_integration.go
  95. 5 5
      api/types/release.go
  96. 1 0
      api/types/request.go
  97. 15 0
      cli/cmd/config.go
  98. 41 2
      cli/cmd/config/config.go
  99. 1 1
      cli/cmd/deploy/create.go
  100. 1 1
      cli/cmd/portforward.go

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

@@ -86,3 +86,65 @@ jobs:
           aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name dev
             
           kubectl rollout restart deployment/provisioner
+  build-push-ecr-server:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v2.3.4
+    - name: Set Github tag
+      id: vars
+      run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
+    - name: Configure AWS credentials
+      uses: aws-actions/configure-aws-credentials@v1
+      with:
+        aws-access-key-id: ${{ secrets.ECR_DEV_AWS_ACCESS_KEY_ID }}
+        aws-secret-access-key: ${{ secrets.ECR_DEV_AWS_ACCESS_SECRET_KEY }}
+        aws-region: us-east-2
+    - name: Login to ECR
+      id: login-ecr
+      run: |
+        aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 801172602658.dkr.ecr.us-east-2.amazonaws.com
+    - name: Write Dashboard Environment Variables
+      run: |
+        cat >./dashboard/.env <<EOL
+        NODE_ENV=development
+        API_SERVER=dashboard.dev.getporter.dev
+        DISCORD_KEY=${{secrets.DISCORD_KEY}}
+        DISCORD_CID=${{secrets.DISCORD_CID}}
+        FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
+        APPLICATION_CHART_REPO_URL=https://charts.dev.getporter.dev
+        ADDON_CHART_REPO_URL=https://chart-addons.dev.getporter.dev
+        ENABLE_SENTRY=true
+        SENTRY_DSN=${{secrets.SENTRY_DSN}}
+        SENTRY_ENV=frontend-development
+        EOL
+    - name: Build
+      run: |
+        DOCKER_BUILDKIT=1 docker build . -t 801172602658.dkr.ecr.us-east-2.amazonaws.com/porter:${{ steps.vars.outputs.sha_short }} -f ./ee/docker/ee.Dockerfile
+    - name: Push to ECR
+      run: |
+        docker push 801172602658.dkr.ecr.us-east-2.amazonaws.com/porter:${{ steps.vars.outputs.sha_short }}
+  build-push-ecr-provisioner:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v2.3.4
+    - name: Set Github tag
+      id: vars
+      run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
+    - name: Configure AWS credentials
+      uses: aws-actions/configure-aws-credentials@v1
+      with:
+        aws-access-key-id: ${{ secrets.ECR_DEV_AWS_ACCESS_KEY_ID }}
+        aws-secret-access-key: ${{ secrets.ECR_DEV_AWS_ACCESS_SECRET_KEY }}
+        aws-region: us-east-2
+    - name: Login to ECR
+      id: login-ecr
+      run: |
+        aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 801172602658.dkr.ecr.us-east-2.amazonaws.com
+    - name: Build
+      run: |
+        DOCKER_BUILDKIT=1 docker build . -t 801172602658.dkr.ecr.us-east-2.amazonaws.com/provisioner-service:${{ steps.vars.outputs.sha_short }} -f ./ee/docker/provisioner.Dockerfile
+    - name: Push to ECR
+      run: |
+        docker push 801172602658.dkr.ecr.us-east-2.amazonaws.com/provisioner-service:${{ steps.vars.outputs.sha_short }}

+ 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",

+ 38 - 7
api/server/authn/handler.go

@@ -11,6 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/models"
 )
 
 // AuthNFactory generates a middleware handler `AuthN`
@@ -62,7 +63,7 @@ func (authn *AuthN) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		authn.sendForbiddenError(err, w, r)
 		return
 	} else if err == nil && tok != nil {
-		authn.nextWithToken(w, r, tok)
+		authn.verifyTokenWithNext(w, r, tok)
 		return
 	}
 
@@ -120,13 +121,43 @@ func (authn *AuthN) handleForbiddenForSession(
 	return
 }
 
-// nextWithToken calls the next handler with either the service account or user corresponding
-// to the token set in context.
-func (authn *AuthN) nextWithToken(w http.ResponseWriter, r *http.Request, tok *token.Token) {
-	// TODO: add section to get service account for server-side token
+func (authn *AuthN) verifyTokenWithNext(w http.ResponseWriter, r *http.Request, tok *token.Token) {
+	// if the token has a stored token id and secret we check that the token is valid in the database
+	if tok.Secret != "" && tok.TokenID != "" {
+		apiToken, err := authn.config.Repo.APIToken().ReadAPIToken(tok.ProjectID, tok.TokenID)
 
-	// for now, we just use nextWithUser using the `iby` field for the token
-	authn.nextWithUserID(w, r, tok.IBy)
+		if err != nil {
+			authn.sendForbiddenError(fmt.Errorf("token with id %s not valid", tok.TokenID), w, r)
+			return
+		}
+
+		// first ensure that the token hasn't been revoked, and the token has not expired
+		if apiToken.Revoked || apiToken.IsExpired() {
+			authn.sendForbiddenError(fmt.Errorf("token with id %s not valid", tok.TokenID), w, r)
+			return
+		}
+
+		authn.nextWithAPIToken(w, r, apiToken)
+	} else {
+		// otherwise we just use nextWithUser using the `iby` field for the token
+		authn.nextWithUserID(w, r, tok.IBy)
+	}
+}
+
+// nextWithAPIToken sets the token in context
+func (authn *AuthN) nextWithAPIToken(w http.ResponseWriter, r *http.Request, tok *models.APIToken) {
+	ctx := r.Context()
+	ctx = context.WithValue(ctx, "api_token", tok)
+
+	// add a service account user to the project: note that any calls depending on a DB lookup for the
+	// user will fail
+	ctx = context.WithValue(ctx, types.UserScope, &models.User{
+		Email:         fmt.Sprintf("%s-%d", tok.Name, tok.ProjectID),
+		EmailVerified: true,
+	})
+
+	r = r.Clone(ctx)
+	authn.next.ServeHTTP(w, r)
 }
 
 // nextWithUserID calls the next handler with the user set in the context with key

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

+ 23 - 5
api/server/authz/policy.go

@@ -47,11 +47,27 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// load policy documents for the user + project
-	projID := reqScopes[types.ProjectScope].Resource.UInt
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	policyLoaderOpts := &policy.PolicyLoaderOpts{}
+
+	// first check if an api token exists in context
+	if r.Context().Value("api_token") != nil {
+		projID := reqScopes[types.ProjectScope].Resource.UInt
+
+		// FIXME: find a clean way to get the project
+
+		apiToken, _ := r.Context().Value("api_token").(*models.APIToken)
+		policyLoaderOpts.ProjectToken = apiToken
+		policyLoaderOpts.ProjectID = projID
+	} else {
+		projID := reqScopes[types.ProjectScope].Resource.UInt
+		user, _ := r.Context().Value(types.UserScope).(*models.User)
 
-	policyDocs, reqErr := h.loader.LoadPolicyDocuments(user.ID, projID)
+		policyLoaderOpts.ProjectID = projID
+		policyLoaderOpts.UserID = user.ID
+	}
+
+	// load policy documents for the user + project
+	policyDocs, reqErr := h.loader.LoadPolicyDocuments(policyLoaderOpts)
 
 	if reqErr != nil {
 		apierrors.HandleAPIError(h.config.Logger, h.config.Alerter, w, r, reqErr, true)
@@ -67,7 +83,7 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			h.config.Alerter,
 			w,
 			r,
-			apierrors.NewErrForbidden(fmt.Errorf("policy forbids action for user %d in project %d", user.ID, projID)),
+			apierrors.NewErrForbidden(fmt.Errorf("policy forbids action in project %d", policyLoaderOpts.ProjectID)),
 			true,
 		)
 
@@ -116,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 {

+ 107 - 58
api/server/authz/policy/loader.go

@@ -1,85 +1,134 @@
 package policy
 
 import (
+	"errors"
 	"fmt"
+	"net/http"
 
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
 )
 
+type PolicyLoaderOpts struct {
+	ProjectID, UserID uint
+	ProjectToken      *models.APIToken
+}
+
 type PolicyDocumentLoader interface {
-	LoadPolicyDocuments(userID, projectID uint) ([]*types.PolicyDocument, apierrors.RequestError)
+	LoadPolicyDocuments(opts *PolicyLoaderOpts) ([]*types.PolicyDocument, apierrors.RequestError)
 }
 
-// BasicPolicyDocumentLoader loads policy documents simply depending on the
-type BasicPolicyDocumentLoader struct {
-	projRepo repository.ProjectRepository
+// RepoPolicyDocumentLoader loads policy documents by reading from the repository database
+type RepoPolicyDocumentLoader struct {
+	projRepo   repository.ProjectRepository
+	policyRepo repository.PolicyRepository
 }
 
-func NewBasicPolicyDocumentLoader(projRepo repository.ProjectRepository) *BasicPolicyDocumentLoader {
-	return &BasicPolicyDocumentLoader{projRepo}
+func NewBasicPolicyDocumentLoader(projRepo repository.ProjectRepository, policyRepo repository.PolicyRepository) *RepoPolicyDocumentLoader {
+	return &RepoPolicyDocumentLoader{projRepo, policyRepo}
 }
 
-func (b *BasicPolicyDocumentLoader) LoadPolicyDocuments(
-	userID, projectID uint,
+func (b *RepoPolicyDocumentLoader) LoadPolicyDocuments(
+	opts *PolicyLoaderOpts,
 ) ([]*types.PolicyDocument, apierrors.RequestError) {
-	// read role and case on role "kind"
-	role, err := b.projRepo.ReadProjectRole(projectID, userID)
-
-	if err != nil && err == gorm.ErrRecordNotFound {
-		return nil, apierrors.NewErrForbidden(
-			fmt.Errorf("user %d does not have a role in project %d", userID, projectID),
-		)
-	} else if err != nil {
-		return nil, apierrors.NewErrInternal(err)
-	}
+	if opts.ProjectToken != nil {
+		// check that the token belongs to the project, in this case it's solely project-scoped
+		if opts.ProjectID == 0 || opts.ProjectToken.ProjectID == 0 || opts.ProjectID != opts.ProjectToken.ProjectID {
+			return nil, apierrors.NewErrForbidden(fmt.Errorf("project id %d does not match token id %d", opts.ProjectID, opts.ProjectToken.ProjectID))
+		}
 
-	// load role based on role kind
-	switch role.Kind {
-	case types.RoleAdmin:
-		return AdminPolicy, nil
-	case types.RoleDeveloper:
-		return DeveloperPolicy, nil
-	case types.RoleViewer:
-		return ViewerPolicy, nil
-	default:
-		return nil, apierrors.NewErrForbidden(
-			fmt.Errorf("%s role not supported for user %d, project %d", string(role.Kind), userID, projectID),
-		)
+		// load the policy
+		apiPolicy, reqErr := GetAPIPolicyFromUID(b.policyRepo, opts.ProjectToken.ProjectID, opts.ProjectToken.PolicyUID)
+
+		if reqErr != nil {
+			return nil, reqErr
+		}
+
+		return apiPolicy.Policy, nil
+	} else if opts.ProjectID != 0 && opts.UserID != 0 {
+		userID := opts.UserID
+		projectID := opts.ProjectID
+		// read role and case on role "kind"
+		role, err := b.projRepo.ReadProjectRole(projectID, userID)
+
+		if err != nil && err == gorm.ErrRecordNotFound {
+			return nil, apierrors.NewErrForbidden(
+				fmt.Errorf("user %d does not have a role in project %d", userID, projectID),
+			)
+		} else if err != nil {
+			return nil, apierrors.NewErrInternal(err)
+		}
+
+		// load role based on role kind
+		switch role.Kind {
+		case types.RoleAdmin:
+			return types.AdminPolicy, nil
+		case types.RoleDeveloper:
+			return types.DeveloperPolicy, nil
+		case types.RoleViewer:
+			return types.ViewerPolicy, nil
+		default:
+			return nil, apierrors.NewErrForbidden(
+				fmt.Errorf("%s role not supported for user %d, project %d", string(role.Kind), userID, projectID),
+			)
+		}
 	}
-}
 
-var AdminPolicy = []*types.PolicyDocument{
-	{
-		Scope: types.ProjectScope,
-		Verbs: types.ReadWriteVerbGroup(),
-	},
+	return nil, apierrors.NewErrForbidden(
+		fmt.Errorf("policy loader called with invalid arguments"),
+	)
 }
 
-var DeveloperPolicy = []*types.PolicyDocument{
-	{
-		Scope: types.ProjectScope,
-		Verbs: types.ReadWriteVerbGroup(),
-		Children: map[types.PermissionScope]*types.PolicyDocument{
-			types.SettingsScope: {
-				Scope: types.SettingsScope,
-				Verbs: types.ReadVerbGroup(),
+func GetAPIPolicyFromUID(policyRepo repository.PolicyRepository, projectID uint, uid string) (*types.APIPolicy, apierrors.RequestError) {
+	switch uid {
+	case "admin":
+		return &types.APIPolicy{
+			APIPolicyMeta: &types.APIPolicyMeta{
+				Name: "admin",
+				UID:  "admin",
 			},
-		},
-	},
-}
-
-var ViewerPolicy = []*types.PolicyDocument{
-	{
-		Scope: types.ProjectScope,
-		Verbs: types.ReadVerbGroup(),
-		Children: map[types.PermissionScope]*types.PolicyDocument{
-			types.SettingsScope: {
-				Scope: types.SettingsScope,
-				Verbs: []types.APIVerb{},
+			Policy: types.AdminPolicy,
+		}, nil
+	case "developer":
+		return &types.APIPolicy{
+			APIPolicyMeta: &types.APIPolicyMeta{
+				Name: "developer",
+				UID:  "developer",
+			},
+			Policy: types.DeveloperPolicy,
+		}, nil
+	case "viewer":
+		return &types.APIPolicy{
+			APIPolicyMeta: &types.APIPolicyMeta{
+				Name: "viewer",
+				UID:  "viewer",
 			},
-		},
-	},
+			Policy: types.ViewerPolicy,
+		}, nil
+	default:
+		// look up the policy and make sure it exists
+		policyModel, err := policyRepo.ReadPolicy(projectID, uid)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				return nil, apierrors.NewErrPassThroughToClient(
+					fmt.Errorf("policy not found in project"),
+					http.StatusBadRequest,
+				)
+			}
+
+			return nil, apierrors.NewErrInternal(err)
+		}
+
+		apiPolicy, err := policyModel.ToAPIPolicyType()
+
+		if err != nil {
+			return nil, apierrors.NewErrInternal(err)
+		}
+
+		return apiPolicy, nil
+	}
 }

+ 18 - 9
api/server/authz/policy/loader_test.go

@@ -26,17 +26,17 @@ var basicLoaderTests = []basicLoaderTest{
 	{
 		description: "should load admin policy",
 		roleKind:    types.RoleAdmin,
-		expPolicy:   policy.AdminPolicy,
+		expPolicy:   types.AdminPolicy,
 	},
 	{
 		description: "should load developer policy",
 		roleKind:    types.RoleDeveloper,
-		expPolicy:   policy.DeveloperPolicy,
+		expPolicy:   types.DeveloperPolicy,
 	},
 	{
 		description: "should load viewer policy",
 		roleKind:    types.RoleViewer,
-		expPolicy:   policy.ViewerPolicy,
+		expPolicy:   types.ViewerPolicy,
 	},
 	{
 		description:      "should not load custom policy for basic loader",
@@ -53,7 +53,7 @@ func TestBasicPolicyDocumentLoader(t *testing.T) {
 	for _, basicTest := range basicLoaderTests {
 		// use the in-memory project repo
 		projRepo := test.NewProjectRepository(true)
-		loader := policy.NewBasicPolicyDocumentLoader(projRepo)
+		loader := policy.NewBasicPolicyDocumentLoader(projRepo, nil)
 
 		project := &models.Project{
 			Name: "test-project",
@@ -79,7 +79,10 @@ func TestBasicPolicyDocumentLoader(t *testing.T) {
 			t.Fatalf("%v", err)
 		}
 
-		docs, reqErr := loader.LoadPolicyDocuments(1, 1)
+		docs, reqErr := loader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
+			ProjectID: 1,
+			UserID:    1,
+		})
 
 		assert.Equal(
 			reqErr != nil,
@@ -123,7 +126,7 @@ func TestErrorForbiddenInvalidRole(t *testing.T) {
 
 	// use the in-memory project repo
 	projRepo := test.NewProjectRepository(true)
-	loader := policy.NewBasicPolicyDocumentLoader(projRepo)
+	loader := policy.NewBasicPolicyDocumentLoader(projRepo, nil)
 
 	project := &models.Project{
 		Name: "test-project",
@@ -149,7 +152,10 @@ func TestErrorForbiddenInvalidRole(t *testing.T) {
 		t.Fatalf("%v", err)
 	}
 
-	_, reqErr := loader.LoadPolicyDocuments(2, 1)
+	_, reqErr := loader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
+		ProjectID: 1,
+		UserID:    2,
+	})
 
 	if reqErr == nil {
 		t.Fatalf("Expected forbidden error for invalid project role")
@@ -174,9 +180,12 @@ func TestErrorCannotQuery(t *testing.T) {
 
 	// use the in-memory project repo
 	projRepo := test.NewProjectRepository(false)
-	loader := policy.NewBasicPolicyDocumentLoader(projRepo)
+	loader := policy.NewBasicPolicyDocumentLoader(projRepo, nil)
 
-	_, reqErr := loader.LoadPolicyDocuments(2, 1)
+	_, reqErr := loader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
+		ProjectID: 2,
+		UserID:    1,
+	})
 
 	if reqErr == nil {
 		t.Fatalf("Expected internal error for failing to query")

+ 11 - 1
api/server/authz/policy/policy.go

@@ -117,6 +117,16 @@ func populateAndVerifyPolicyDocument(
 
 	processedChildren := 0
 
+	// by default, we pass the parent's verbs to the child.
+	passedParentVerbs := currDoc.Verbs
+
+	// however, if the current scope is a project scope, we don't pass the parent's verbs to
+	// the child. This is to avoid additional verbs being added later as a child of the ProjectScope,
+	// which would unintentionally grant permission to future-added scopes.
+	if currScope == types.ProjectScope {
+		passedParentVerbs = []types.APIVerb{}
+	}
+
 	for currScope := range subTree {
 		if _, exists := currDoc.Children[currScope]; exists {
 			processedChildren++
@@ -126,7 +136,7 @@ func populateAndVerifyPolicyDocument(
 			currDoc.Children[currScope],
 			subTree,
 			currScope,
-			currDoc.Verbs,
+			passedParentVerbs,
 			reqScopes,
 			matchDocs,
 		)

+ 3 - 3
api/server/authz/policy/policy_test.go

@@ -18,7 +18,7 @@ type testHasScopeAccess struct {
 var hasScopeAccessTests = []testHasScopeAccess{
 	{
 		description: "admin access to project",
-		policy:      policy.AdminPolicy,
+		policy:      types.AdminPolicy,
 		reqScopes: map[types.PermissionScope]*types.RequestAction{
 			types.ProjectScope: {
 				Verb: types.APIVerbGet,
@@ -31,7 +31,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	},
 	{
 		description: "viewer access cannot perform write operation",
-		policy:      policy.ViewerPolicy,
+		policy:      types.ViewerPolicy,
 		reqScopes: map[types.PermissionScope]*types.RequestAction{
 			types.ClusterScope: {
 				Verb: types.APIVerbCreate,
@@ -44,7 +44,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	},
 	{
 		description: "developer access cannot write settings",
-		policy:      policy.DeveloperPolicy,
+		policy:      types.DeveloperPolicy,
 		reqScopes: map[types.PermissionScope]*types.RequestAction{
 			types.SettingsScope: {
 				Verb: types.APIVerbUpdate,

+ 4 - 4
api/server/authz/policy_test.go

@@ -240,7 +240,7 @@ func loadHandlers(
 	shouldLoaderLoadViewer bool,
 ) (*config.Config, http.Handler, *testHandler) {
 	config := apitest.LoadConfig(t)
-	var loader policy.PolicyDocumentLoader = policy.NewBasicPolicyDocumentLoader(config.Repo.Project())
+	var loader policy.PolicyDocumentLoader = policy.NewBasicPolicyDocumentLoader(config.Repo.Project(), config.Repo.Policy())
 
 	if shouldLoaderFail {
 		loader = &failingDocLoader{}
@@ -260,14 +260,14 @@ func loadHandlers(
 
 type failingDocLoader struct{}
 
-func (f *failingDocLoader) LoadPolicyDocuments(userID, projectID uint) ([]*types.PolicyDocument, apierrors.RequestError) {
+func (f *failingDocLoader) LoadPolicyDocuments(opts *policy.PolicyLoaderOpts) ([]*types.PolicyDocument, apierrors.RequestError) {
 	return nil, apierrors.NewErrInternal(fmt.Errorf("new error internal"))
 }
 
 type viewerDocLoader struct{}
 
-func (f *viewerDocLoader) LoadPolicyDocuments(userID, projectID uint) ([]*types.PolicyDocument, apierrors.RequestError) {
-	return policy.ViewerPolicy, nil
+func (f *viewerDocLoader) LoadPolicyDocuments(opts *policy.PolicyLoaderOpts) ([]*types.PolicyDocument, apierrors.RequestError) {
+	return types.ViewerPolicy, nil
 }
 
 type testHandler struct {

+ 118 - 0
api/server/handlers/api_token/create.go

@@ -0,0 +1,118 @@
+package api_token
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/porter-dev/porter/api/server/authz/policy"
+	"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/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/encryption"
+	"github.com/porter-dev/porter/internal/models"
+	"golang.org/x/crypto/bcrypt"
+)
+
+type APITokenCreateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewAPITokenCreateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *APITokenCreateHandler {
+	return &APITokenCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *APITokenCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	if !proj.APITokensEnabled {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
+		return
+	}
+
+	req := &types.CreateAPIToken{}
+
+	if ok := p.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	// if the expiry time is not set, set the expiry to 1 year
+	if req.ExpiresAt.IsZero() {
+		req.ExpiresAt = time.Now().Add(time.Hour * 24 * 365)
+	}
+
+	apiPolicy, reqErr := policy.GetAPIPolicyFromUID(p.Repo().Policy(), proj.ID, req.PolicyUID)
+
+	if reqErr != nil {
+		p.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	uid, err := encryption.GenerateRandomBytes(16)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	secretKey, err := encryption.GenerateRandomBytes(16)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// hash the secret key for storage in the db
+	hashedToken, err := bcrypt.GenerateFromPassword([]byte(secretKey), 8)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	apiToken := &models.APIToken{
+		UniqueID:        uid,
+		ProjectID:       proj.ID,
+		CreatedByUserID: user.ID,
+		Expiry:          &req.ExpiresAt,
+		Revoked:         false,
+		PolicyUID:       apiPolicy.UID,
+		PolicyName:      apiPolicy.Name,
+		Name:            req.Name,
+		SecretKey:       hashedToken,
+	}
+
+	apiToken, err = p.Repo().APIToken().CreateAPIToken(apiToken)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// generate porter jwt token
+	jwt, err := token.GetStoredTokenForAPI(user.ID, proj.ID, apiToken.UniqueID, secretKey)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	encoded, err := jwt.EncodeToken(p.Config().TokenConf)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	p.WriteResult(w, r, apiToken.ToAPITokenType(apiPolicy.Policy, encoded))
+}

+ 72 - 0
api/server/handlers/api_token/get.go

@@ -0,0 +1,72 @@
+package api_token
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz/policy"
+	"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/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type APITokenGetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewAPITokenGetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *APITokenGetHandler {
+	return &APITokenGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *APITokenGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	if !proj.APITokensEnabled {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
+		return
+	}
+
+	// get the token id from the request
+	tokenID, reqErr := requestutils.GetURLParamString(r, types.URLParamTokenID)
+
+	if reqErr != nil {
+		p.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	token, err := p.Repo().APIToken().ReadAPIToken(proj.ID, tokenID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("token with id %s not found in project", tokenID),
+				http.StatusNotFound,
+			))
+			return
+		}
+
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	apiPolicy, reqErr := policy.GetAPIPolicyFromUID(p.Repo().Policy(), proj.ID, token.PolicyUID)
+
+	if reqErr != nil {
+		p.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	p.WriteResult(w, r, token.ToAPITokenType(apiPolicy.Policy, ""))
+}

+ 51 - 0
api/server/handlers/api_token/list.go

@@ -0,0 +1,51 @@
+package api_token
+
+import (
+	"fmt"
+	"net/http"
+
+	"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/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type APITokenListHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewAPITokenListHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *APITokenListHandler {
+	return &APITokenListHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *APITokenListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	if !proj.APITokensEnabled {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
+		return
+	}
+
+	tokens, err := p.Repo().APIToken().ListAPITokensByProjectID(proj.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	apiTokens := make([]*types.APITokenMeta, 0)
+
+	for _, tok := range tokens {
+		apiTokens = append(apiTokens, tok.ToAPITokenMetaType())
+	}
+
+	p.WriteResult(w, r, apiTokens)
+}

+ 73 - 0
api/server/handlers/api_token/revoke.go

@@ -0,0 +1,73 @@
+package api_token
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"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/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type APITokenRevokeHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewAPITokenRevokeHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *APITokenRevokeHandler {
+	return &APITokenRevokeHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *APITokenRevokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	if !proj.APITokensEnabled {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
+		return
+	}
+
+	// get the token id from the request
+	tokenID, reqErr := requestutils.GetURLParamString(r, types.URLParamTokenID)
+
+	if reqErr != nil {
+		p.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	token, err := p.Repo().APIToken().ReadAPIToken(proj.ID, tokenID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("token with id %s not found in project", tokenID),
+				http.StatusNotFound,
+			))
+			return
+		}
+
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	token.Revoked = true
+
+	token, err = p.Repo().APIToken().UpdateAPIToken(token)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	p.WriteResult(w, r, token.ToAPITokenMetaType())
+}

+ 1 - 0
api/server/handlers/cluster/create_namespace.go

@@ -55,5 +55,6 @@ func (c *CreateNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		Namespace: namespace,
 	}
 
+	w.WriteHeader(http.StatusCreated)
 	c.WriteResult(w, r, res)
 }

+ 11 - 4
api/server/handlers/cluster/delete_namespace.go

@@ -8,6 +8,7 @@ import (
 	"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/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -28,10 +29,16 @@ func NewDeleteNamespaceHandler(
 }
 
 func (c *DeleteNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	request := &types.DeleteNamespaceRequest{}
+	namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
 
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		return
+	if namespace == "" {
+		request := &types.DeleteNamespaceRequest{}
+
+		if ok := c.DecodeAndValidate(w, r, request); !ok {
+			return
+		}
+
+		namespace = request.Name
 	}
 
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
@@ -43,7 +50,7 @@ func (c *DeleteNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	if err := agent.DeleteNamespace(request.Name); err != nil {
+	if err := agent.DeleteNamespace(namespace); err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 61 - 0
api/server/handlers/cluster/get_namespace.go

@@ -0,0 +1,61 @@
+package cluster
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"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/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"k8s.io/apimachinery/pkg/api/errors"
+)
+
+type GetNamespaceHandler struct {
+	handlers.PorterHandlerWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetNamespaceHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetNamespaceHandler {
+	return &GetNamespaceHandler{
+		PorterHandlerWriter:   handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *GetNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	namespace, reqErr := requestutils.GetURLParamString(r, types.URLParamNamespace)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
+	}
+
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	agent, err := c.GetAgent(r, cluster, "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res, err := agent.GetNamespace(namespace)
+
+	if err != nil {
+		if errors.IsNotFound(err) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(err))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 22 - 5
api/server/handlers/environment/create.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -9,9 +10,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 +42,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
@@ -134,8 +135,18 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	})
 
 	if err != nil {
-		c.deleteEnvAndReportError(w, r, env, err)
-		return
+		unwrappedErr := errors.Unwrap(err)
+
+		if unwrappedErr != nil {
+			if errors.Is(unwrappedErr, actions.ErrProtectedBranch) {
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
+			} else if errors.Is(unwrappedErr, actions.ErrCreatePRForProtectedBranch) {
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusPreconditionFailed))
+			}
+		} else {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 	}
 
 	c.WriteResult(w, r, env.ToEnvironmentType())
@@ -144,7 +155,13 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 func (c *CreateEnvironmentHandler) deleteEnvAndReportError(
 	w http.ResponseWriter, r *http.Request, env *models.Environment, err error,
 ) {
-	c.Repo().Environment().DeleteEnvironment(env)
+	_, delErr := c.Repo().Environment().DeleteEnvironment(env)
+
+	if delErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(delErr))
+		return
+	}
+
 	c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 }
 

+ 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

+ 28 - 18
api/server/handlers/environment/delete.go

@@ -1,13 +1,15 @@
 package environment
 
 import (
+	"errors"
+	"fmt"
 	"net/http"
 
 	"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 +38,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
@@ -58,22 +60,6 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	err = actions.DeleteEnv(&actions.EnvOpts{
-		Client:            client,
-		ServerURL:         c.Config().ServerConf.ServerURL,
-		GitRepoOwner:      env.GitRepoOwner,
-		GitRepoName:       env.GitRepoName,
-		ProjectID:         project.ID,
-		ClusterID:         cluster.ID,
-		GitInstallationID: uint(ga.InstallationID),
-		EnvironmentName:   env.Name,
-	})
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	// delete all corresponding deployments
 	agent, err := c.GetAgent(r, cluster, "")
 
@@ -101,5 +87,29 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	err = actions.DeleteEnv(&actions.EnvOpts{
+		Client:            client,
+		ServerURL:         c.Config().ServerConf.ServerURL,
+		GitRepoOwner:      env.GitRepoOwner,
+		GitRepoName:       env.GitRepoName,
+		ProjectID:         project.ID,
+		ClusterID:         cluster.ID,
+		GitInstallationID: uint(ga.InstallationID),
+		EnvironmentName:   env.Name,
+	})
+
+	if err != nil {
+		if errors.Is(err, actions.ErrProtectedBranch) {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("We were unable to delete the Porter Preview Environment workflow files for this "+
+					"repository as the default branch is protected. Please manually delete them."), http.StatusConflict,
+			))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	c.WriteResult(w, r, env.ToEnvironmentType())
 }

+ 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

+ 4 - 3
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
@@ -103,7 +104,7 @@ func (c *GithubGetBuildpackHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 					return
 				}
 			}()
-			buildpacks.Runtimes[idx].Detect(
+			buildpacks.Runtimes[idx].DetectGithub(
 				client, directoryContents, owner, name, request.Dir, repoContentOptions,
 				builderInfoMap[buildpacks.PaketoBuilder], builderInfoMap[buildpacks.HerokuBuilder],
 			)

+ 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

+ 31 - 2
api/server/handlers/handler.go

@@ -17,7 +17,14 @@ type PorterHandler interface {
 	Repo() repository.Repository
 	HandleAPIError(w http.ResponseWriter, r *http.Request, err apierrors.RequestError)
 	HandleAPIErrorNoWrite(w http.ResponseWriter, r *http.Request, err apierrors.RequestError)
-	PopulateOAuthSession(w http.ResponseWriter, r *http.Request, state string, isProject bool) error
+	PopulateOAuthSession(
+		w http.ResponseWriter,
+		r *http.Request,
+		state string,
+		isUser, isProject bool,
+		integrationClient types.OAuthIntegrationClient,
+		integrationID uint,
+	) error
 }
 
 type PorterHandlerWriter interface {
@@ -81,7 +88,14 @@ func IgnoreAPIError(w http.ResponseWriter, r *http.Request, err apierrors.Reques
 	return
 }
 
-func (d *DefaultPorterHandler) PopulateOAuthSession(w http.ResponseWriter, r *http.Request, state string, isProject bool) error {
+func (d *DefaultPorterHandler) PopulateOAuthSession(
+	w http.ResponseWriter,
+	r *http.Request,
+	state string,
+	isProject, isUser bool,
+	integrationClient types.OAuthIntegrationClient,
+	integrationID uint,
+) error {
 	session, err := d.Config().Store.Get(r, d.Config().ServerConf.CookieName)
 
 	if err != nil {
@@ -106,6 +120,21 @@ func (d *DefaultPorterHandler) PopulateOAuthSession(w http.ResponseWriter, r *ht
 		session.Values["project_id"] = project.ID
 	}
 
+	if isUser {
+		user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+		if user == nil {
+			return fmt.Errorf("could not read user")
+		}
+
+		session.Values["user_id"] = user.ID
+	}
+
+	if integrationID != 0 && len(integrationClient) > 0 {
+		session.Values["integration_id"] = integrationID
+		session.Values["integration_client"] = string(integrationClient)
+	}
+
 	if err := session.Save(r, w); err != nil {
 		return err
 	}

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

@@ -81,6 +81,8 @@ func (c *InfraCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		CreatedByUserID: user.ID,
 		SourceLink:      sourceLink,
 		SourceVersion:   sourceVersion,
+		// If the cluster ID was passed in, we store the parent cluster ID in the infra
+		// so it can be referenced later
 		ParentClusterID: req.ClusterID,
 	}
 
@@ -201,6 +203,8 @@ func getSourceLinkAndVersion(kind types.InfraKind) (string, string) {
 		return "porter/aws/eks", "v0.1.0"
 	case types.InfraRDS:
 		return "porter/aws/rds", "v0.1.0"
+	case types.InfraS3:
+		return "porter/aws/s3", "v0.1.0"
 	case types.InfraGCR:
 		return "porter/gcp/gcr", "v0.1.0"
 	case types.InfraGKE:

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

@@ -18,6 +18,27 @@ tabs:
         default: hello
 `
 
+const s3Form = `name: S3
+hasSource: false
+includeHiddenFields: true
+isClusterScoped: true
+tabs:
+- name: main
+  label: Main
+  sections:
+  - name: heading
+    contents: 
+    - type: heading
+      label: S3 Settings
+  - name: bucket_name
+    contents:
+    - type: string-input
+      label: Bucket Name
+      required: true
+      placeholder: "s3-bucket-name"
+      variable: bucket_name
+`
+
 const rdsForm = `name: RDS
 hasSource: false
 includeHiddenFields: true

+ 2 - 0
api/server/handlers/infra/get_template.go

@@ -67,6 +67,8 @@ func getFormBytesFromKind(kind string) []byte {
 		formBytes = []byte(ecrForm)
 	case "rds":
 		formBytes = []byte(rdsForm)
+	case "s3":
+		formBytes = []byte(s3Form)
 	case "eks":
 		formBytes = []byte(eksForm)
 	case "gcr":

+ 8 - 0
api/server/handlers/infra/list_templates.go

@@ -57,6 +57,14 @@ var templateMap = map[string]*types.InfraTemplateMeta{
 		Kind:               "rds",
 		RequiredCredential: "aws_integration_id",
 	},
+	"s3": {
+		Icon:               "",
+		Description:        "Create an S3 bucket.",
+		Name:               "S3",
+		Version:            "v0.1.0",
+		Kind:               "s3",
+		RequiredCredential: "aws_integration_id",
+	},
 	"eks": {
 		Icon:               "https://img.stackshare.io/service/7991/amazon-eks.png",
 		Description:        "Create an Elastic Kubernetes Service cluster.",

+ 4 - 0
api/server/handlers/namespace/list_releases.go

@@ -35,6 +35,10 @@ func (c *ListReleasesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	if request.ReleaseListFilter == nil {
+		request.ReleaseListFilter = &types.ReleaseListFilter{}
+	}
+
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 

+ 132 - 0
api/server/handlers/oauth_callback/gitlab.go

@@ -0,0 +1,132 @@
+package oauth_callback
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"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/models/integrations"
+	"gorm.io/gorm"
+)
+
+type OAuthCallbackGitlabHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewOAuthCallbackGitlabHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *OAuthCallbackGitlabHandler {
+	return &OAuthCallbackGitlabHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *OAuthCallbackGitlabHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	session, err := p.Config().Store.Get(r, p.Config().ServerConf.CookieName)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if _, ok := session.Values["state"]; !ok {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if r.URL.Query().Get("state") != session.Values["state"] {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	userID, _ := session.Values["user_id"].(uint)
+	projID, _ := session.Values["project_id"].(uint)
+	integrationID := session.Values["integration_id"].(uint)
+
+	giIntegration, err := p.Repo().GitlabIntegration().ReadGitlabIntegration(projID, integrationID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			p.HandleAPIError(w, r, apierrors.NewErrForbidden(
+				fmt.Errorf("gitlab integration with id %d not found in project %d",
+					integrationID, projID),
+			))
+			return
+		}
+
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	token, err := commonutils.GetGitlabOAuthConf(p.Config(), giIntegration).
+		Exchange(context.Background(), r.URL.Query().Get("code"))
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	if !token.Valid() {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("invalid token")))
+		return
+	}
+
+	oauthInt := &integrations.OAuthIntegration{
+		SharedOAuthModel: integrations.SharedOAuthModel{
+			AccessToken:  []byte(token.AccessToken),
+			RefreshToken: []byte(token.RefreshToken),
+			Expiry:       token.Expiry,
+		},
+		Client:    types.OAuthGitlab,
+		UserID:    userID,
+		ProjectID: projID,
+	}
+
+	oauthInt, err = p.Repo().OAuthIntegration().CreateOAuthIntegration(oauthInt)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if oauthInt.ID == 0 {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating oauth integration for gitlab")))
+		return
+	}
+
+	giOAuthInt := &integrations.GitlabAppOAuthIntegration{
+		OAuthIntegrationID:  oauthInt.ID,
+		GitlabIntegrationID: integrationID,
+	}
+
+	// create the oauth integration first
+	_, err = p.Repo().GitlabAppOAuthIntegration().CreateGitlabAppOAuthIntegration(giOAuthInt)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if redirectStr, ok := session.Values["redirect_uri"].(string); ok && redirectStr != "" {
+		// attempt to parse the redirect uri, if it fails just redirect to dashboard
+		redirectURI, err := url.Parse(redirectStr)
+
+		if err != nil {
+			http.Redirect(w, r, "/dashboard", http.StatusFound)
+		}
+
+		http.Redirect(w, r, fmt.Sprintf("%s?%s", redirectURI.Path, redirectURI.RawQuery), http.StatusFound)
+	} else {
+		http.Redirect(w, r, "/dashboard", http.StatusFound)
+	}
+}

+ 89 - 0
api/server/handlers/policy/create.go

@@ -0,0 +1,89 @@
+package policy
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"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/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/encryption"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type PolicyCreateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewPolicyCreateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *PolicyCreateHandler {
+	return &PolicyCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *PolicyCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	req := &types.CreatePolicy{}
+
+	if ok := p.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	// policy can't be one of the preset policy names
+	if name := strings.ToLower(req.Name); name == "admin" || name == "developer" || name == "viewer" {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("name cannot be one of the preset policy names"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	uid, err := encryption.GenerateRandomBytes(16)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	policyBytes, err := json.Marshal(req.Policy)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	policy := &models.Policy{
+		ProjectID:       proj.ID,
+		UniqueID:        uid,
+		CreatedByUserID: user.ID,
+		Name:            req.Name,
+		PolicyBytes:     policyBytes,
+	}
+
+	policy, err = p.Repo().Policy().CreatePolicy(policy)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res, err := policy.ToAPIPolicyType()
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 66 - 0
api/server/handlers/policy/get.go

@@ -0,0 +1,66 @@
+package policy
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"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/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type PolicyGetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewPolicyGetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *PolicyGetHandler {
+	return &PolicyGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *PolicyGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	// get the token id from the request
+	policyID, reqErr := requestutils.GetURLParamString(r, types.URLParamPolicyID)
+
+	if reqErr != nil {
+		p.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	policy, err := p.Repo().Policy().ReadPolicy(proj.ID, policyID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("policy with id %s not found in project", policyID),
+				http.StatusNotFound,
+			))
+			return
+		}
+
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res, err := policy.ToAPIPolicyType()
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 45 - 0
api/server/handlers/policy/list.go

@@ -0,0 +1,45 @@
+package policy
+
+import (
+	"net/http"
+
+	"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/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type PolicyListHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewPolicyListHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *PolicyListHandler {
+	return &PolicyListHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *PolicyListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	policies, err := p.Repo().Policy().ListPoliciesByProjectID(proj.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := make([]*types.APIPolicyMeta, 0)
+
+	for _, policy := range policies {
+		res = append(res, policy.ToAPIPolicyTypeMeta())
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 5 - 2
api/server/handlers/project/get_policy.go

@@ -30,9 +30,12 @@ func (p *ProjectGetPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	policyDocLoader := policy.NewBasicPolicyDocumentLoader(p.Repo().Project())
+	policyDocLoader := policy.NewBasicPolicyDocumentLoader(p.Config().Repo.Project(), p.Config().Repo.Policy())
 
-	policyDocs, err := policyDocLoader.LoadPolicyDocuments(user.ID, proj.ID)
+	policyDocs, err := policyDocLoader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
+		UserID:    user.ID,
+		ProjectID: proj.ID,
+	})
 
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))

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

@@ -0,0 +1,73 @@
+package project_integration
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"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/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+type CreateGitlabIntegration struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewCreateGitlabIntegration(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateGitlabIntegration {
+	return &CreateGitlabIntegration{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+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 {
+		return
+	}
+
+	_, err := url.Parse(request.InstanceURL)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("malformed gitlab instance URL"), http.StatusBadRequest,
+		))
+		return
+	}
+
+	gitlabIntegration, err := p.Repo().GitlabIntegration().CreateGitlabIntegration(&ints.GitlabIntegration{
+		ProjectID:       project.ID,
+		InstanceURL:     request.InstanceURL,
+		AppClientID:     []byte(request.AppClientID),
+		AppClientSecret: []byte(request.AppClientSecret),
+	})
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := types.CreateGitlabResponse{
+		GitlabIntegration: gitlabIntegration.ToGitlabIntegrationType(),
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 160 - 0
api/server/handlers/project_integration/get_gitlab_repo_buildpack.go

@@ -0,0 +1,160 @@
+package project_integration
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"sync"
+
+	"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"
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/xanzy/go-gitlab"
+)
+
+type GetGitlabRepoBuildpackHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewGetGitlabRepoBuildpackHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetGitlabRepoBuildpackHandler {
+	return &GetGitlabRepoBuildpackHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+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{}
+
+	ok := p.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
+
+	if !ok {
+		return
+	}
+
+	branch, ok := commonutils.GetBranchParam(p, w, r)
+
+	if !ok {
+		return
+	}
+
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
+
+	if err != nil {
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
+		}
+
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	dir, err := url.QueryUnescape(request.Dir)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("malformed query param dir")))
+		return
+	}
+
+	dir = strings.TrimPrefix(dir, "./")
+
+	if len(dir) == 0 {
+		dir = "."
+	}
+
+	tree, resp, err := client.Repositories.ListTree(fmt.Sprintf("%s/%s", owner, name), &gitlab.ListTreeOptions{
+		Path: gitlab.String(dir),
+		Ref:  gitlab.String(branch),
+	})
+
+	if resp.StatusCode == http.StatusUnauthorized {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
+		return
+	} else if resp.StatusCode == http.StatusNotFound {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("no such gitlab project found"), http.StatusNotFound))
+		return
+	}
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	builderInfoMap := initBuilderInfo()
+	var wg sync.WaitGroup
+	wg.Add(len(buildpacks.Runtimes))
+	for i := range buildpacks.Runtimes {
+		go func(idx int) {
+			defer func() {
+				if rec := recover(); rec != nil {
+					p.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("panic detected in runtime detection")))
+					return
+				}
+			}()
+			buildpacks.Runtimes[idx].DetectGitlab(
+				client, tree, owner, name, dir, branch,
+				builderInfoMap[buildpacks.PaketoBuilder], builderInfoMap[buildpacks.HerokuBuilder],
+			)
+			wg.Done()
+		}(i)
+	}
+	wg.Wait()
+
+	// FIXME: add Java buildpacks
+	builderInfoMap[buildpacks.PaketoBuilder].Others = append(builderInfoMap[buildpacks.PaketoBuilder].Others,
+		buildpacks.BuildpackInfo{
+			Name:      "Java",
+			Buildpack: "gcr.io/paketo-buildpacks/java",
+		})
+	builderInfoMap[buildpacks.HerokuBuilder].Others = append(builderInfoMap[buildpacks.HerokuBuilder].Others,
+		buildpacks.BuildpackInfo{
+			Name:      "Java",
+			Buildpack: "heroku/java",
+		})
+
+	var builders []*buildpacks.BuilderInfo
+	for _, v := range builderInfoMap {
+		builders = append(builders, v)
+	}
+
+	p.WriteResult(w, r, builders)
+}
+
+func initBuilderInfo() map[string]*buildpacks.BuilderInfo {
+	builders := make(map[string]*buildpacks.BuilderInfo)
+	builders[buildpacks.PaketoBuilder] = &buildpacks.BuilderInfo{
+		Name: "Paketo",
+		Builders: []string{
+			"paketobuildpacks/builder:full",
+		},
+	}
+	builders[buildpacks.HerokuBuilder] = &buildpacks.BuilderInfo{
+		Name: "Heroku",
+		Builders: []string{
+			"heroku/buildpacks:20",
+			"heroku/buildpacks:18",
+		},
+	}
+	return builders
+}

+ 112 - 0
api/server/handlers/project_integration/get_gitlab_repo_contents.go

@@ -0,0 +1,112 @@
+package project_integration
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"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/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/xanzy/go-gitlab"
+)
+
+type GetGitlabRepoContentsHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewGetGitlabRepoContentsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetGitlabRepoContentsHandler {
+	return &GetGitlabRepoContentsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+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{}
+
+	ok := p.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
+
+	if !ok {
+		return
+	}
+
+	branch, ok := commonutils.GetBranchParam(p, w, r)
+
+	if !ok {
+		return
+	}
+
+	dir, err := url.QueryUnescape(request.Dir)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("malformed query param dir")))
+		return
+	}
+
+	dir = strings.TrimPrefix(dir, "./")
+
+	if len(dir) == 0 {
+		dir = "."
+	}
+
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
+
+	if err != nil {
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
+		}
+
+		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),
+	})
+
+	if resp.StatusCode == http.StatusUnauthorized {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
+		return
+	} else if resp.StatusCode == http.StatusNotFound {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("no such gitlab project found"), http.StatusNotFound))
+		return
+	}
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res types.GetContentsResponse
+
+	for _, node := range tree {
+		res = append(res, types.GithubDirectoryItem{
+			Path: node.Path,
+			Type: node.Type,
+		})
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 111 - 0
api/server/handlers/project_integration/get_gitlab_repo_procfile.go

@@ -0,0 +1,111 @@
+package project_integration
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strings"
+
+	"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/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/xanzy/go-gitlab"
+)
+
+var procfileRegex = regexp.MustCompile("^([A-Za-z0-9_]+):\\s*(.+)$")
+
+type GetGitlabRepoProcfileHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewGetGitlabRepoProcfileHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetGitlabRepoProcfileHandler {
+	return &GetGitlabRepoProcfileHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+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{}
+
+	ok := p.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
+
+	if !ok {
+		return
+	}
+
+	branch, ok := commonutils.GetBranchParam(p, w, r)
+
+	if !ok {
+		return
+	}
+
+	path, err := url.QueryUnescape(request.Path)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("malformed query param path")))
+		return
+	}
+
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
+
+	if err != nil {
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
+		}
+
+		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),
+		},
+	)
+
+	if resp.StatusCode == http.StatusUnauthorized {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
+		return
+	} else if resp.StatusCode == http.StatusNotFound {
+		w.WriteHeader(http.StatusNotFound)
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("no such procfile exists")))
+		return
+	}
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	parsedContents := make(types.GetProcfileResponse)
+
+	// parse the procfile information
+	for _, line := range strings.Split(string(file), "\n") {
+		if matches := procfileRegex.FindStringSubmatch(line); matches != nil {
+			parsedContents[matches[1]] = matches[2]
+		}
+	}
+
+	p.WriteResult(w, r, parsedContents)
+}

+ 117 - 0
api/server/handlers/project_integration/list_git.go

@@ -0,0 +1,117 @@
+package project_integration
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/google/go-github/v39/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/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type ListGitIntegrationHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewListGitIntegrationHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ListGitIntegrationHandler {
+	return &ListGitIntegrationHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *ListGitIntegrationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	gitlabInts, err := p.Repo().GitlabIntegration().ListGitlabIntegrationsByProjectID(project.ID)
+
+	var res types.ListGitIntegrationResponse
+
+	if err == nil {
+		for _, gitlabInt := range gitlabInts {
+			res = append(res, &types.GitIntegration{
+				Provider:      "gitlab",
+				InstanceURL:   gitlabInt.InstanceURL,
+				IntegrationID: gitlabInt.ID,
+			})
+		}
+	}
+
+	tok, err := gitinstallation.GetGithubAppOauthTokenFromRequest(p.Config(), r)
+
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			// return empty array, this is not an error
+			p.WriteResult(w, r, res)
+		} else {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		}
+
+		return
+	}
+
+	client := github.NewClient(p.Config().GithubAppConf.Client(context.Background(), tok))
+
+	var accountIDs []int64
+	accountIDMap := make(map[int64]string)
+
+	ghAuthUser, _, err := client.Users.Get(context.Background(), "")
+
+	if err != nil {
+		p.WriteResult(w, r, res)
+		return
+	}
+
+	accountIDs = append(accountIDs, ghAuthUser.GetID())
+	accountIDMap[ghAuthUser.GetID()] = ghAuthUser.GetLogin()
+
+	opts := &github.ListOptions{
+		PerPage: 100,
+		Page:    1,
+	}
+
+	for {
+		orgs, pages, err := client.Organizations.List(context.Background(), "", opts)
+
+		if err != nil {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			p.WriteResult(w, r, res)
+			return
+		}
+
+		for _, org := range orgs {
+			accountIDs = append(accountIDs, org.GetID())
+			accountIDMap[org.GetID()] = org.GetLogin()
+		}
+
+		if pages.NextPage == 0 {
+			break
+		}
+	}
+
+	installationData, err := p.Repo().GithubAppInstallation().ReadGithubAppInstallationByAccountIDs(accountIDs)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		p.WriteResult(w, r, res)
+	}
+
+	for _, data := range installationData {
+		res = append(res, &types.GitIntegration{
+			Provider:       "github",
+			Name:           accountIDMap[data.AccountID],
+			InstallationID: data.InstallationID,
+		})
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 44 - 0
api/server/handlers/project_integration/list_gitlab.go

@@ -0,0 +1,44 @@
+package project_integration
+
+import (
+	"net/http"
+
+	"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/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ListGitlabHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewListGitlabHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListGitlabHandler {
+	return &ListGitlabHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *ListGitlabHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	gitlabInts, err := p.Repo().GitlabIntegration().ListGitlabIntegrationsByProjectID(project.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res types.ListGitlabResponse = make([]*types.GitlabIntegration, 0)
+
+	for _, gitlabInt := range gitlabInts {
+		res = append(res, gitlabInt.ToGitlabIntegrationType())
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 77 - 0
api/server/handlers/project_integration/list_gitlab_repo_branches.go

@@ -0,0 +1,77 @@
+package project_integration
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"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/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/xanzy/go-gitlab"
+)
+
+type ListGitlabRepoBranchesHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewListGitlabRepoBranchesHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ListGitlabRepoBranchesHandler {
+	return &ListGitlabRepoBranchesHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+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)
+
+	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
+
+	if !ok {
+		return
+	}
+
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
+
+	if err != nil {
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
+		}
+
+		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 {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
+		return
+	} else if resp.StatusCode == http.StatusNotFound {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("no such gitlab project found"), http.StatusNotFound))
+		return
+	}
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res []string
+
+	for _, branch := range branches {
+		res = append(res, branch.Name)
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 119 - 0
api/server/handlers/project_integration/list_gitlab_repos.go

@@ -0,0 +1,119 @@
+package project_integration
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"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/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
+}
+
+func NewListGitlabReposHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ListGitlabReposHandler {
+	return &ListGitlabReposHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+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())
+
+	if err != nil {
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
+		}
+
+		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
+	}
+
+	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) {
+			return nil, errUnauthorizedGitlabUser
+		}
+
+		return nil, err
+	}
+
+	oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(projectID, giAppOAuth.OAuthIntegrationID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, errUnauthorizedGitlabUser
+		}
+
+		return nil, err
+	}
+
+	accessToken, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, commonutils.GetGitlabOAuthConf(
+		config, gi,
+	), oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(projectID, giAppOAuth, repo))
+
+	if err != nil {
+		return nil, errUnauthorizedGitlabUser
+	}
+
+	client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
+
+	if err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}

+ 1 - 1
api/server/handlers/project_oauth/digitalocean.go

@@ -29,7 +29,7 @@ func NewProjectOAuthDOHandler(
 func (p *ProjectOAuthDOHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	state := oauth.CreateRandomState()
 
-	if err := p.PopulateOAuthSession(w, r, state, true); err != nil {
+	if err := p.PopulateOAuthSession(w, r, state, false, true, "", 0); err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 78 - 0
api/server/handlers/project_oauth/gitlab.go

@@ -0,0 +1,78 @@
+package project_oauth
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"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/models"
+	"github.com/porter-dev/porter/internal/oauth"
+	"golang.org/x/oauth2"
+	"gorm.io/gorm"
+)
+
+type ProjectOAuthGitlabHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewProjectOAuthGitlabHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ProjectOAuthGitlabHandler {
+	return &ProjectOAuthGitlabHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *ProjectOAuthGitlabHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	integrationIDStr := r.URL.Query().Get("integration_id")
+
+	if len(integrationIDStr) == 0 {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("required query param integration_id")))
+		return
+	}
+
+	integrationID, err := strconv.ParseUint(integrationIDStr, 10, 32)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	giIntegration, err := p.Repo().GitlabIntegration().ReadGitlabIntegration(proj.ID, uint(integrationID))
+
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			p.HandleAPIError(w, r, apierrors.NewErrForbidden(
+				fmt.Errorf("gitlab integration with id %d not found in project %d", integrationID, proj.ID),
+			))
+		} else {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		}
+
+		return
+	}
+
+	state := oauth.CreateRandomState()
+
+	if err := p.PopulateOAuthSession(w, r, state, true, true, types.OAuthGitlab, uint(integrationID)); err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	gitlabConf := commonutils.GetGitlabOAuthConf(p.Config(), giIntegration)
+
+	// specify access type offline to get a refresh token
+	url := gitlabConf.AuthCodeURL(state, oauth2.AccessTypeOffline)
+
+	http.Redirect(w, r, url, http.StatusFound)
+}

+ 1 - 1
api/server/handlers/project_oauth/slack.go

@@ -29,7 +29,7 @@ func NewProjectOAuthSlackHandler(
 func (p *ProjectOAuthSlackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	state := oauth.CreateRandomState()
 
-	if err := p.PopulateOAuthSession(w, r, state, true); err != nil {
+	if err := p.PopulateOAuthSession(w, r, state, false, true, "", 0); err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 206 - 67
api/server/handlers/release/create.go

@@ -2,9 +2,11 @@ package release
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
+	"time"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -18,9 +20,11 @@ import (
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
+	"github.com/porter-dev/porter/internal/integrations/ci/gitlab"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/registry"
+	"golang.org/x/crypto/bcrypt"
 	"gopkg.in/yaml.v2"
 	"helm.sh/helm/v3/pkg/release"
 	v1 "k8s.io/api/core/v1"
@@ -160,33 +164,43 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		}
 	}
 
-	if request.GithubActionConfig != nil {
+	if request.BuildConfig != nil {
+		_, err = createBuildConfig(c.Config(), release, request.BuildConfig)
+	}
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if request.GitActionConfig != nil {
 		_, _, err := createGitAction(
 			c.Config(),
 			user.ID,
 			cluster.ProjectID,
 			cluster.ID,
-			request.GithubActionConfig,
+			request.GitActionConfig,
 			request.Name,
 			namespace,
 			release,
 		)
 
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
+			unwrappedErr := errors.Unwrap(err)
+
+			if unwrappedErr != nil {
+				if errors.Is(unwrappedErr, actions.ErrProtectedBranch) {
+					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
+				} else if errors.Is(unwrappedErr, actions.ErrCreatePRForProtectedBranch) {
+					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusPreconditionFailed))
+				}
+			} else {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
 		}
 	}
 
-	if request.BuildConfig != nil {
-		_, err = createBuildConfig(c.Config(), release, request.BuildConfig)
-	}
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	c.Config().AnalyticsClient.Track(analytics.ApplicationLaunchSuccessTrack(
 		&analytics.ApplicationLaunchSuccessTrackOpts{
 			ApplicationScopedTrackOpts: analytics.GetApplicationScopedTrackOpts(
@@ -269,75 +283,101 @@ func createGitAction(
 		}
 	}
 
+	isDryRun := release == nil
+
 	repoSplit := strings.Split(request.GitRepo, "/")
 
 	if len(repoSplit) != 2 {
 		return nil, nil, fmt.Errorf("invalid formatting of repo name")
 	}
 
-	// generate porter jwt token
-	jwt, err := token.GetTokenForAPI(userID, projectID)
+	encoded := ""
+	var err error
 
-	if err != nil {
-		return nil, nil, err
+	// if this isn't a dry run, generate the token
+	if !isDryRun {
+		encoded, err = getToken(config, userID, projectID, clusterID, request)
+
+		if err != nil {
+			return nil, nil, err
+		}
 	}
 
-	encoded, err := jwt.EncodeToken(config.TokenConf)
+	var workflowYAML []byte
+	var gitErr error
+
+	if request.GitlabIntegrationID != 0 {
+		giRunner := &gitlab.GitlabCI{
+			ServerURL:        config.ServerConf.ServerURL,
+			GitRepoOwner:     repoSplit[0],
+			GitRepoName:      repoSplit[1],
+			GitBranch:        request.GitBranch,
+			Repo:             config.Repo,
+			ProjectID:        projectID,
+			ClusterID:        clusterID,
+			UserID:           userID,
+			IntegrationID:    request.GitlabIntegrationID,
+			PorterConf:       config,
+			ReleaseName:      name,
+			ReleaseNamespace: namespace,
+			FolderPath:       request.FolderPath,
+			PorterToken:      encoded,
+		}
 
-	if err != nil {
-		return nil, nil, err
-	}
+		gitErr = giRunner.Setup()
+	} else {
+		// create the commit in the git repo
+		gaRunner := &actions.GithubActions{
+			InstanceName:           config.ServerConf.InstanceName,
+			ServerURL:              config.ServerConf.ServerURL,
+			GithubOAuthIntegration: nil,
+			GithubAppID:            config.GithubAppConf.AppID,
+			GithubAppSecretPath:    config.GithubAppConf.SecretPath,
+			GithubInstallationID:   request.GitRepoID,
+			GitRepoName:            repoSplit[1],
+			GitRepoOwner:           repoSplit[0],
+			Repo:                   config.Repo,
+			ProjectID:              projectID,
+			ClusterID:              clusterID,
+			ReleaseName:            name,
+			ReleaseNamespace:       namespace,
+			GitBranch:              request.GitBranch,
+			DockerFilePath:         request.DockerfilePath,
+			FolderPath:             request.FolderPath,
+			ImageRepoURL:           request.ImageRepoURI,
+			PorterToken:            encoded,
+			Version:                "v0.1.0",
+			ShouldCreateWorkflow:   request.ShouldCreateWorkflow,
+			DryRun:                 release == nil,
+		}
 
-	// create the commit in the git repo
-	gaRunner := &actions.GithubActions{
-		InstanceName:           config.ServerConf.InstanceName,
-		ServerURL:              config.ServerConf.ServerURL,
-		GithubOAuthIntegration: nil,
-		GithubAppID:            config.GithubAppConf.AppID,
-		GithubAppSecretPath:    config.GithubAppConf.SecretPath,
-		GithubInstallationID:   request.GitRepoID,
-		GitRepoName:            repoSplit[1],
-		GitRepoOwner:           repoSplit[0],
-		Repo:                   config.Repo,
-		ProjectID:              projectID,
-		ClusterID:              clusterID,
-		ReleaseName:            name,
-		ReleaseNamespace:       namespace,
-		GitBranch:              request.GitBranch,
-		DockerFilePath:         request.DockerfilePath,
-		FolderPath:             request.FolderPath,
-		ImageRepoURL:           request.ImageRepoURI,
-		PorterToken:            encoded,
-		Version:                "v0.1.0",
-		ShouldCreateWorkflow:   request.ShouldCreateWorkflow,
-		DryRun:                 release == nil,
-	}
+		// Save the github err for after creating the git action config. However, we
+		// need to call Setup() in order to get the workflow file before writing the
+		// action config, in the case of a dry run, since the dry run does not create
+		// a git action config.
+		workflowYAML, gitErr = gaRunner.Setup()
 
-	// Save the github err for after creating the git action config. However, we
-	// need to call Setup() in order to get the workflow file before writing the
-	// action config, in the case of a dry run, since the dry run does not create
-	// a git action config.
-	workflowYAML, githubErr := gaRunner.Setup()
+		if gaRunner.DryRun {
+			if gitErr != nil {
+				return nil, nil, gitErr
+			}
 
-	if gaRunner.DryRun {
-		if githubErr != nil {
-			return nil, nil, githubErr
+			return nil, workflowYAML, nil
 		}
-
-		return nil, workflowYAML, nil
 	}
 
 	// handle write to the database
 	ga, err := config.Repo.GitActionConfig().CreateGitActionConfig(&models.GitActionConfig{
-		ReleaseID:      release.ID,
-		GitRepo:        request.GitRepo,
-		GitBranch:      request.GitBranch,
-		ImageRepoURI:   request.ImageRepoURI,
-		GitRepoID:      request.GitRepoID,
-		DockerfilePath: request.DockerfilePath,
-		FolderPath:     request.FolderPath,
-		IsInstallation: true,
-		Version:        "v0.1.0",
+		ReleaseID:           release.ID,
+		GitRepo:             request.GitRepo,
+		GitBranch:           request.GitBranch,
+		ImageRepoURI:        request.ImageRepoURI,
+		GitRepoID:           request.GitRepoID,
+		GitlabIntegrationID: request.GitlabIntegrationID,
+		DockerfilePath:      request.DockerfilePath,
+		FolderPath:          request.FolderPath,
+		IsInstallation:      true,
+		Version:             "v0.1.0",
 	})
 
 	if err != nil {
@@ -353,11 +393,110 @@ func createGitAction(
 		return nil, nil, err
 	}
 
-	if githubErr != nil {
-		return nil, nil, githubErr
+	return ga.ToGitActionConfigType(), workflowYAML, gitErr
+}
+
+func getToken(
+	config *config.Config,
+	userID, projectID, clusterID uint,
+	request *types.CreateGitActionConfigRequest,
+) (string, error) {
+	// create a policy for the token
+	policy := []*types.PolicyDocument{
+		{
+			Scope: types.ProjectScope,
+			Verbs: types.ReadWriteVerbGroup(),
+			Children: map[types.PermissionScope]*types.PolicyDocument{
+				types.ClusterScope: {
+					Scope: types.ClusterScope,
+					Verbs: types.ReadWriteVerbGroup(),
+				},
+				types.RegistryScope: {
+					Scope: types.RegistryScope,
+					Verbs: types.ReadVerbGroup(),
+				},
+				types.HelmRepoScope: {
+					Scope: types.HelmRepoScope,
+					Verbs: types.ReadVerbGroup(),
+				},
+			},
+		},
+	}
+
+	uid, err := encryption.GenerateRandomBytes(16)
+
+	if err != nil {
+		return "", err
+	}
+
+	policyBytes, err := json.Marshal(policy)
+
+	if err != nil {
+		return "", err
+	}
+
+	policyModel := &models.Policy{
+		ProjectID:       projectID,
+		UniqueID:        uid,
+		CreatedByUserID: userID,
+		Name:            strings.ToLower(fmt.Sprintf("repo-%s-token-policy", request.GitRepo)),
+		PolicyBytes:     policyBytes,
+	}
+
+	policyModel, err = config.Repo.Policy().CreatePolicy(policyModel)
+
+	if err != nil {
+		return "", err
+	}
+
+	// create the token in the database
+	tokenUID, err := encryption.GenerateRandomBytes(16)
+
+	if err != nil {
+		return "", err
+	}
+
+	secretKey, err := encryption.GenerateRandomBytes(16)
+
+	if err != nil {
+		return "", err
+	}
+
+	// hash the secret key for storage in the db
+	hashedToken, err := bcrypt.GenerateFromPassword([]byte(secretKey), 8)
+
+	if err != nil {
+		return "", err
+	}
+
+	expiresAt := time.Now().Add(time.Hour * 24 * 365)
+
+	apiToken := &models.APIToken{
+		UniqueID:        tokenUID,
+		ProjectID:       projectID,
+		CreatedByUserID: userID,
+		Expiry:          &expiresAt,
+		Revoked:         false,
+		PolicyUID:       policyModel.UniqueID,
+		PolicyName:      policyModel.Name,
+		Name:            strings.ToLower(fmt.Sprintf("repo-%s-token", request.GitRepo)),
+		SecretKey:       hashedToken,
+	}
+
+	apiToken, err = config.Repo.APIToken().CreateAPIToken(apiToken)
+
+	if err != nil {
+		return "", err
+	}
+
+	// generate porter jwt token
+	jwt, err := token.GetStoredTokenForAPI(userID, projectID, apiToken.UniqueID, secretKey)
+
+	if err != nil {
+		return "", err
 	}
 
-	return ga.ToGitActionConfigType(), workflowYAML, nil
+	return jwt.EncodeToken(config.TokenConf)
 }
 
 func createBuildConfig(

+ 53 - 20
api/server/handlers/release/delete.go

@@ -1,7 +1,9 @@
 package release
 
 import (
+	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -9,6 +11,7 @@ import (
 	"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/integrations/ci/gitlab"
 	"github.com/porter-dev/porter/internal/models"
 	"helm.sh/helm/v3/pkg/release"
 )
@@ -56,28 +59,58 @@ func (c *DeleteReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 			gitAction := rel.GitActionConfig
 
 			if gitAction != nil && gitAction.ID != 0 {
-				gaRunner, err := getGARunner(
-					c.Config(),
-					user.ID,
-					cluster.ProjectID,
-					cluster.ID,
-					rel.GitActionConfig,
-					helmRelease.Name,
-					helmRelease.Namespace,
-					rel,
-					helmRelease,
-				)
-
-				if err != nil {
-					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-					return
-				}
+				if gitAction.GitlabIntegrationID != 0 {
+					repoSplit := strings.Split(gitAction.GitRepo, "/")
+
+					if len(repoSplit) != 2 {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("invalid formatting of repo name")))
+						return
+					}
+
+					giRunner := &gitlab.GitlabCI{
+						ServerURL:        c.Config().ServerConf.ServerURL,
+						GitRepoOwner:     repoSplit[0],
+						GitRepoName:      repoSplit[1],
+						Repo:             c.Repo(),
+						ProjectID:        cluster.ProjectID,
+						ClusterID:        cluster.ID,
+						UserID:           user.ID,
+						IntegrationID:    gitAction.GitlabIntegrationID,
+						PorterConf:       c.Config(),
+						ReleaseName:      helmRelease.Name,
+						ReleaseNamespace: helmRelease.Namespace,
+					}
+
+					err = giRunner.Cleanup()
+
+					if err != nil {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+						return
+					}
+				} else {
+					gaRunner, err := getGARunner(
+						c.Config(),
+						user.ID,
+						cluster.ProjectID,
+						cluster.ID,
+						rel.GitActionConfig,
+						helmRelease.Name,
+						helmRelease.Namespace,
+						rel,
+						helmRelease,
+					)
+
+					if err != nil {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+						return
+					}
 
-				err = gaRunner.Cleanup()
+					err = gaRunner.Cleanup()
 
-				if err != nil {
-					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-					return
+					if err != nil {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+						return
+					}
 				}
 			}
 		}

+ 41 - 0
api/server/handlers/release/get.go

@@ -125,6 +125,12 @@ func (c *ReleaseGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		if res.Release.Chart.Name() == "cert-manager" {
 			formYAML, err := parser.FormYAMLFromBytes(parserDef, []byte(certManagerForm), "", "")
 
+			if err == nil {
+				res.Form = formYAML
+			}
+		} else if res.Release.Chart.Name() == "velero" {
+			formYAML, err := parser.FormYAMLFromBytes(parserDef, []byte(veleroForm), "", "")
+
 			if err == nil {
 				res.Form = formYAML
 			}
@@ -184,3 +190,38 @@ tabs:
           message: [.status.conditions[].message] | unique | join(","),
           data: {}
         }`
+
+const veleroForm string = `tags:
+- hello
+tabs:
+- name: main
+  context:
+    type: cluster
+    config:
+      group: velero.io
+      version: v1
+      resource: backups
+  label: Backups
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: 💾 Velero Backups
+    - type: resource-list
+      value: |
+        .items[] | { 
+          name: .metadata.name, 
+          label: .metadata.namespace,
+          status: .status.phase,
+          timestamp: .status.completionTimestamp,
+          message: [
+            (if .status.volumeSnapshotsAttempted then "\(.status.volumeSnapshotsAttempted) volume snapshots attempted, \(.status.volumeSnapshotsCompleted) completed." else null end),
+            "Finished \(.status.completionTimestamp).",
+            "Backup expires on \(.status.expiration)."
+          ]|join(" "),
+          data: {
+            "Included Namespaces": (if .spec.includedNamespaces then .spec.includedNamespaces|join(",") else "* (all)" end),
+            "Included Resources": (if .spec.includedResources then .spec.includedResources|join(",") else "* (all)" end),
+            "Storage Location": .spec.storageLocation
+          }
+        }`

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

@@ -195,7 +195,7 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 
 			gitAction := rel.GitActionConfig
 
-			if gitAction != nil && gitAction.ID != 0 {
+			if gitAction != nil && gitAction.ID != 0 && gitAction.GitlabIntegrationID == 0 {
 				gaRunner, err := getGARunner(
 					c.Config(),
 					user.ID,

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

@@ -74,7 +74,7 @@ func (c *RollbackReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 			gitAction := rel.GitActionConfig
 
-			if gitAction != nil && gitAction.ID != 0 {
+			if gitAction != nil && gitAction.ID != 0 && gitAction.GitlabIntegrationID == 0 {
 				gaRunner, err := getGARunner(
 					c.Config(),
 					user.ID,

+ 1 - 1
api/server/handlers/user/github_start.go

@@ -29,7 +29,7 @@ func NewUserOAuthGithubHandler(
 func (p *UserOAuthGithubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	state := oauth.CreateRandomState()
 
-	if err := p.PopulateOAuthSession(w, r, state, false); err != nil {
+	if err := p.PopulateOAuthSession(w, r, state, false, false, "", 0); err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 1 - 1
api/server/handlers/user/google_start.go

@@ -29,7 +29,7 @@ func NewUserOAuthGoogleHandler(
 func (p *UserOAuthGoogleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	state := oauth.CreateRandomState()
 
-	if err := p.PopulateOAuthSession(w, r, state, false); err != nil {
+	if err := p.PopulateOAuthSession(w, r, state, false, false, "", 0); err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 28 - 27
api/server/router/base.go

@@ -14,11 +14,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/webhook"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewBaseRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewBaseRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetBaseRoutes,
 		Children:  children,
 	}
@@ -29,9 +30,9 @@ func GetBaseRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
-	routes := make([]*Route, 0)
+	children ...*router.Registerer,
+) []*router.Route {
+	routes := make([]*router.Route, 0)
 
 	// GET /api/readyz -> healthcheck.NewReadyzHandler
 	getReadyzEndpoint := factory.NewAPIEndpoint(
@@ -51,7 +52,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getReadyzEndpoint,
 		Handler:  getReadyzHandler,
 		Router:   r,
@@ -75,7 +76,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getLivezEndpoint,
 		Handler:  getLivezHandler,
 		Router:   r,
@@ -98,7 +99,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getMetadataEndpoint,
 		Handler:  getMetadataHandler,
 		Router:   r,
@@ -121,7 +122,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listClusterIntsEndpoint,
 		Handler:  listClusterIntsHandler,
 		Router:   r,
@@ -144,7 +145,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listRegistryIntsEndpoint,
 		Handler:  listRegistryIntsHandler,
 		Router:   r,
@@ -167,7 +168,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listHelmRepoIntsEndpoint,
 		Handler:  listHelmRepoIntsHandler,
 		Router:   r,
@@ -191,7 +192,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createUserEndpoint,
 		Handler:  createUserHandler,
 		Router:   r,
@@ -215,7 +216,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: loginUserEndpoint,
 		Handler:  loginUserHandler,
 		Router:   r,
@@ -239,7 +240,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: cliLoginExchangeEndpoint,
 		Handler:  cliLoginExchangeHandler,
 		Router:   r,
@@ -263,7 +264,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: passwordInitiateResetEndpoint,
 		Handler:  passwordInitiateResetHandler,
 		Router:   r,
@@ -287,7 +288,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: passwordVerifyResetEndpoint,
 		Handler:  passwordVerifyResetHandler,
 		Router:   r,
@@ -311,7 +312,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: passwordFinalizeResetEndpoint,
 		Handler:  passwordFinalizeResetHandler,
 		Router:   r,
@@ -336,7 +337,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: webhookEndpoint,
 		Handler:  webhookHandler,
 		Router:   r,
@@ -359,7 +360,7 @@ func GetBaseRoutes(
 		config,
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: githubAppInstallEndpoint,
 		Handler:  githubAppInstallHandler,
 		Router:   r,
@@ -384,7 +385,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: githubAppWebhookEndpoint,
 		Handler:  githubAppWebhookHandler,
 		Router:   r,
@@ -409,7 +410,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: githubLoginStartEndpoint,
 		Handler:  githubLoginStartHandler,
 		Router:   r,
@@ -434,7 +435,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: githubLoginCallbackEndpoint,
 		Handler:  githubLoginCallbackHandler,
 		Router:   r,
@@ -459,7 +460,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: googleLoginStartEndpoint,
 		Handler:  googleLoginStartHandler,
 		Router:   r,
@@ -484,7 +485,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: googleLoginCallbackEndpoint,
 		Handler:  googleLoginCallbackHandler,
 		Router:   r,
@@ -509,7 +510,7 @@ func GetBaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getCredentialsEndpoint,
 		Handler:  getCredentialsHandler,
 		Router:   r,
@@ -533,7 +534,7 @@ func GetBaseRoutes(
 		factory.GetDecoderValidator(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: addProjectBillingEndpoint,
 		Handler:  addProjectBillingHandler,
 		Router:   r,
@@ -559,7 +560,7 @@ func GetBaseRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: githubIncomingWebhookEndpoint,
 			Handler:  githubIncomingWebhookHandler,
 			Router:   r,

+ 46 - 45
api/server/router/cluster.go

@@ -10,11 +10,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/kube_events"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewClusterScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewClusterScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetClusterScopedRoutes,
 		Children:  children,
 	}
@@ -25,8 +26,8 @@ func GetClusterScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getClusterRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -47,7 +48,7 @@ func getClusterRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/clusters/{cluster_id}"
 
 	newPath := &types.Path{
@@ -55,7 +56,7 @@ func getClusterRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// POST /api/projects/{project_id}/clusters -> project.NewCreateClusterManualHandler
 	createEndpoint := factory.NewAPIEndpoint(
@@ -79,7 +80,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createEndpoint,
 		Handler:  createHandler,
 		Router:   r,
@@ -109,7 +110,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createCandidateEndpoint,
 		Handler:  createCandidateHandler,
 		Router:   r,
@@ -136,7 +137,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listCandidatesEndpoint,
 		Handler:  listCandidatesHandler,
 		Router:   r,
@@ -169,7 +170,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: resolveCandidateEndpoint,
 		Handler:  resolveCandidateHandler,
 		Router:   r,
@@ -198,7 +199,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateClusterEndpoint,
 		Handler:  updateClusterHandler,
 		Router:   r,
@@ -226,7 +227,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteClusterEndpoint,
 		Handler:  deleteClusterHandler,
 		Router:   r,
@@ -254,7 +255,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getEndpoint,
 		Handler:  getHandler,
 		Router:   r,
@@ -282,7 +283,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listDatabaseEndpoint,
 		Handler:  listDatabaseHandler,
 		Router:   r,
@@ -312,7 +313,7 @@ func getClusterRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: listEnvEndpoint,
 			Handler:  listEnvHandler,
 			Router:   r,
@@ -341,7 +342,7 @@ func getClusterRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: listDeploymentsEndpoint,
 			Handler:  listDeploymentsHandler,
 			Router:   r,
@@ -370,7 +371,7 @@ func getClusterRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: getDeploymentEndpoint,
 			Handler:  getDeploymentHandler,
 			Router:   r,
@@ -399,7 +400,7 @@ func getClusterRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: reenableDeploymentEndpoint,
 			Handler:  reenableDeploymentHandler,
 			Router:   r,
@@ -428,7 +429,7 @@ func getClusterRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: triggerDeploymentWorkflowEndpoint,
 			Handler:  triggerDeploymentWorkflowHandler,
 			Router:   r,
@@ -457,7 +458,7 @@ func getClusterRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: enablePullRequestEndpoint,
 			Handler:  enablePullRequestHandler,
 			Router:   r,
@@ -487,7 +488,7 @@ func getClusterRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: deleteDeploymentEndpoint,
 			Handler:  deleteDeploymentHandler,
 			Router:   r,
@@ -517,7 +518,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listNamespacesEndpoint,
 		Handler:  listNamespacesHandler,
 		Router:   r,
@@ -545,7 +546,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listNodesEndpoint,
 		Handler:  listNodesHandler,
 		Router:   r,
@@ -573,7 +574,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getNodeEndpoint,
 		Handler:  getNodeHandler,
 		Router:   r,
@@ -602,7 +603,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createNamespaceEndpoint,
 		Handler:  createNamespaceHandler,
 		Router:   r,
@@ -630,7 +631,7 @@ func getClusterRoutes(
 		factory.GetDecoderValidator(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteNamespaceEndpoint,
 		Handler:  deleteNamespaceHandler,
 		Router:   r,
@@ -658,7 +659,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getTemporaryKubeconfigEndpoint,
 		Handler:  getTemporaryKubeconfigHandler,
 		Router:   r,
@@ -683,7 +684,7 @@ func getClusterRoutes(
 
 	detectPrometheusInstalledHandler := cluster.NewDetectPrometheusInstalledHandler(config)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: detectPrometheusInstalledEndpoint,
 		Handler:  detectPrometheusInstalledHandler,
 		Router:   r,
@@ -708,7 +709,7 @@ func getClusterRoutes(
 
 	detectAgentInstalledHandler := cluster.NewDetectAgentInstalledHandler(config, factory.GetResultWriter())
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: detectAgentInstalledEndpoint,
 		Handler:  detectAgentInstalledHandler,
 		Router:   r,
@@ -737,7 +738,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: installAgentEndpoint,
 		Handler:  installAgentHandler,
 		Router:   r,
@@ -766,7 +767,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: upgradeAgentEndpoint,
 		Handler:  upgradeAgentHandler,
 		Router:   r,
@@ -795,7 +796,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listKubeEventsEndpoint,
 		Handler:  listKubeEventsHandler,
 		Router:   r,
@@ -824,7 +825,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getKubeEventEndpoint,
 		Handler:  getKubeEventHandler,
 		Router:   r,
@@ -853,7 +854,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getKubeEventLogsEndpoint,
 		Handler:  getKubeEventLogsHandler,
 		Router:   r,
@@ -882,7 +883,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getKubeEventLogBucketsEndpoint,
 		Handler:  getKubeEventLogBucketsHandler,
 		Router:   r,
@@ -911,7 +912,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createKubeEventsEndpoint,
 		Handler:  createKubeEventsHandler,
 		Router:   r,
@@ -939,7 +940,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listNGINXIngressesEndpoint,
 		Handler:  listNGINXIngressesHandler,
 		Router:   r,
@@ -968,7 +969,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getPodMetricsEndpoint,
 		Handler:  getPodMetricsHandler,
 		Router:   r,
@@ -998,7 +999,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: streamHelmReleaseEndpoint,
 		Handler:  streamHelmReleaseHandler,
 		Router:   r,
@@ -1032,7 +1033,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: streamStatusEndpoint,
 		Handler:  streamStatusHandler,
 		Router:   r,
@@ -1061,7 +1062,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getPodsEndpoint,
 		Handler:  getPodsHandler,
 		Router:   r,
@@ -1090,7 +1091,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getIncidentsEndpoint,
 		Handler:  getIncidentsHandler,
 		Router:   r,
@@ -1119,7 +1120,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getIncidentEventLogsEndpoint,
 		Handler:  getIncidentEventLogsHandler,
 		Router:   r,
@@ -1148,7 +1149,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: notifyNewIncidentEndpoint,
 		Handler:  notifyNewIncidentHandler,
 		Router:   r,
@@ -1177,7 +1178,7 @@ func getClusterRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: notifyResolvedIncidentEndpoint,
 		Handler:  notifyResolvedIncidentHandler,
 		Router:   r,

+ 24 - 23
api/server/router/git_installation.go

@@ -8,11 +8,12 @@ import (
 	"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/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewGitInstallationScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewGitInstallationScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetGitInstallationScopedRoutes,
 		Children:  children,
 	}
@@ -23,8 +24,8 @@ func GetGitInstallationScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getGitInstallationRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -45,7 +46,7 @@ func getGitInstallationRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/gitrepos/{git_installation_id}"
 
 	newPath := &types.Path{
@@ -53,7 +54,7 @@ func getGitInstallationRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/gitrepos/{git_installation_id} -> gitinstallation.NewGitInstallationGetHandler
 	getEndpoint := factory.NewAPIEndpoint(
@@ -77,7 +78,7 @@ func getGitInstallationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getEndpoint,
 		Handler:  getHandler,
 		Router:   r,
@@ -106,7 +107,7 @@ func getGitInstallationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getPermissionsEndpoint,
 		Handler:  getPermissionsHandler,
 		Router:   r,
@@ -144,7 +145,7 @@ func getGitInstallationRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: createEnvironmentEndpoint,
 			Handler:  createEnvironmentHandler,
 			Router:   r,
@@ -180,7 +181,7 @@ func getGitInstallationRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: createDeploymentEndpoint,
 			Handler:  createDeploymentHandler,
 			Router:   r,
@@ -216,7 +217,7 @@ func getGitInstallationRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: getDeploymentEndpoint,
 			Handler:  getDeploymentHandler,
 			Router:   r,
@@ -252,7 +253,7 @@ func getGitInstallationRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: listDeploymentsEndpoint,
 			Handler:  listDeploymentsHandler,
 			Router:   r,
@@ -288,7 +289,7 @@ func getGitInstallationRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: finalizeDeploymentEndpoint,
 			Handler:  finalizeDeploymentHandler,
 			Router:   r,
@@ -324,7 +325,7 @@ func getGitInstallationRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: updateDeploymentEndpoint,
 			Handler:  updateDeploymentHandler,
 			Router:   r,
@@ -360,7 +361,7 @@ func getGitInstallationRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: updateDeploymentStatusEndpoint,
 			Handler:  updateDeploymentStatusHandler,
 			Router:   r,
@@ -396,7 +397,7 @@ func getGitInstallationRoutes(
 			factory.GetResultWriter(),
 		)
 
-		routes = append(routes, &Route{
+		routes = append(routes, &router.Route{
 			Endpoint: deleteEnvironmentEndpoint,
 			Handler:  deleteEnvironmentHandler,
 			Router:   r,
@@ -427,7 +428,7 @@ func getGitInstallationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listReposEndpoint,
 		Handler:  listReposHandler,
 		Router:   r,
@@ -462,7 +463,7 @@ func getGitInstallationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listBranchesEndpoint,
 		Handler:  listBranchesHandler,
 		Router:   r,
@@ -499,7 +500,7 @@ func getGitInstallationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getBuildpackEndpoint,
 		Handler:  getBuildpackHandler,
 		Router:   r,
@@ -536,7 +537,7 @@ func getGitInstallationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getContentsEndpoint,
 		Handler:  getContentsHandler,
 		Router:   r,
@@ -573,7 +574,7 @@ func getGitInstallationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getProcfileEndpoint,
 		Handler:  getProcfileHandler,
 		Router:   r,
@@ -610,7 +611,7 @@ func getGitInstallationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getTarballURLEndpoint,
 		Handler:  getTarballURLHandler,
 		Router:   r,
@@ -646,7 +647,7 @@ func getGitInstallationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: rerunWorkflowEndpoint,
 		Handler:  rerunWorkflowHandler,
 		Router:   r,

+ 10 - 9
api/server/router/helm_repo.go

@@ -5,11 +5,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/helmrepo"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewHelmRepoScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewHelmRepoScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetHelmRepoScopedRoutes,
 		Children:  children,
 	}
@@ -20,8 +21,8 @@ func GetHelmRepoScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getHelmRepoRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -42,7 +43,7 @@ func getHelmRepoRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/helmrepos/{helm_repo_id}"
 
 	newPath := &types.Path{
@@ -50,7 +51,7 @@ func getHelmRepoRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/helmrepos/{helm_repo_id} -> registry.NewHelmRepoGetHandler
 	getEndpoint := factory.NewAPIEndpoint(
@@ -74,7 +75,7 @@ func getHelmRepoRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getEndpoint,
 		Handler:  getHandler,
 		Router:   r,
@@ -103,7 +104,7 @@ func getHelmRepoRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: hrListEndpoint,
 		Handler:  hrListHandler,
 		Router:   r,
@@ -132,7 +133,7 @@ func getHelmRepoRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: chartGetEndpoint,
 		Handler:  chartGetHandler,
 		Router:   r,

+ 20 - 19
api/server/router/infra.go

@@ -8,11 +8,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/infra"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewInfraScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewInfraScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetInfraScopedRoutes,
 		Children:  children,
 	}
@@ -23,8 +24,8 @@ func GetInfraScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getInfraRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -45,7 +46,7 @@ func getInfraRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/infras/{infra_id}"
 
 	newPath := &types.Path{
@@ -53,7 +54,7 @@ func getInfraRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/infra -> project.NewInfraListHandler
 	listInfraEndpoint := factory.NewAPIEndpoint(
@@ -77,7 +78,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listInfraEndpoint,
 		Handler:  listInfraHandler,
 		Router:   r,
@@ -105,7 +106,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getEndpoint,
 		Handler:  getHandler,
 		Router:   r,
@@ -134,7 +135,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: retryCreateEndpoint,
 		Handler:  retryCreateHandler,
 		Router:   r,
@@ -163,7 +164,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateEndpoint,
 		Handler:  updateHandler,
 		Router:   r,
@@ -192,7 +193,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: retryDeleteEndpoint,
 		Handler:  retryDeleteHandler,
 		Router:   r,
@@ -220,7 +221,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listOperationsEndpoint,
 		Handler:  listOperationsHandler,
 		Router:   r,
@@ -249,7 +250,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getOperationEndpoint,
 		Handler:  getOperationHandler,
 		Router:   r,
@@ -279,7 +280,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: streamStateEndpoint,
 		Handler:  streamStateHandler,
 		Router:   r,
@@ -309,7 +310,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: streamLogEndpoint,
 		Handler:  streamLogHandler,
 		Router:   r,
@@ -338,7 +339,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getOperationLogsEndpoint,
 		Handler:  getOperationLogsHandler,
 		Router:   r,
@@ -366,7 +367,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getStateEndpoint,
 		Handler:  getStateHandler,
 		Router:   r,
@@ -395,7 +396,7 @@ func getInfraRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteEndpoint,
 		Handler:  deleteHandler,
 		Router:   r,
@@ -423,7 +424,7 @@ func getInfraRoutes(
 		factory.GetDecoderValidator(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateDBStatusEndpoint,
 		Handler:  updateDBStatusHandler,
 		Router:   r,

+ 12 - 11
api/server/router/invite.go

@@ -5,11 +5,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/invite"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewInviteScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewInviteScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetInviteScopedRoutes,
 		Children:  children,
 	}
@@ -20,8 +21,8 @@ func GetInviteScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getInviteRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -42,7 +43,7 @@ func getInviteRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/invites/{invite_id}"
 
 	newPath := &types.Path{
@@ -50,7 +51,7 @@ func getInviteRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/invites -> invite.NewInvitesListHandler
 	listEndpoint := factory.NewAPIEndpoint(
@@ -74,7 +75,7 @@ func getInviteRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listEndpoint,
 		Handler:  listHandler,
 		Router:   r,
@@ -105,7 +106,7 @@ func getInviteRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createEndpoint,
 		Handler:  createHandler,
 		Router:   r,
@@ -131,7 +132,7 @@ func getInviteRoutes(
 
 	acceptHandler := invite.NewInviteAcceptHandler(config)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: acceptEndpoint,
 		Handler:  acceptHandler,
 		Router:   r,
@@ -160,7 +161,7 @@ func getInviteRoutes(
 		factory.GetDecoderValidator(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateRoleEndpoint,
 		Handler:  updateRoleHandler,
 		Router:   r,
@@ -188,7 +189,7 @@ func getInviteRoutes(
 		config,
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteEndpoint,
 		Handler:  deleteHandler,
 		Router:   r,

+ 28 - 27
api/server/router/namespace.go

@@ -9,11 +9,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/namespace"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewNamespaceScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewNamespaceScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetNamespaceScopedRoutes,
 		Children:  children,
 	}
@@ -24,8 +25,8 @@ func GetNamespaceScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getNamespaceRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -46,7 +47,7 @@ func getNamespaceRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/namespaces/{namespace}"
 
 	newPath := &types.Path{
@@ -54,7 +55,7 @@ func getNamespaceRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/envgroups/list -> namespace.NewListEnvGroupsHandler
 	listEnvGroupsEndpoint := factory.NewAPIEndpoint(
@@ -79,7 +80,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listEnvGroupsEndpoint,
 		Handler:  listEnvGroupsHandler,
 		Router:   r,
@@ -109,7 +110,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: cloneEnvGroupEndpoint,
 		Handler:  cloneEnvGroupHandler,
 		Router:   r,
@@ -139,7 +140,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getEnvGroupEndpoint,
 		Handler:  getEnvGroupHandler,
 		Router:   r,
@@ -169,7 +170,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getEnvGroupAllVersionsEndpoint,
 		Handler:  getEnvGroupAllVersionsHandler,
 		Router:   r,
@@ -199,7 +200,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createEnvGroupEndpoint,
 		Handler:  createEnvGroupHandler,
 		Router:   r,
@@ -229,7 +230,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateEnvGroupAppsEndpoint,
 		Handler:  updateEnvGroupAppsHandler,
 		Router:   r,
@@ -259,7 +260,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: removeEnvGroupAppEndpoint,
 		Handler:  removeEnvGroupAppHandler,
 		Router:   r,
@@ -289,7 +290,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteEnvGroupEndpoint,
 		Handler:  deleteEnvGroupHandler,
 		Router:   r,
@@ -319,7 +320,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateConfigMapEndpoint,
 		Handler:  updateConfigMapHandler,
 		Router:   r,
@@ -348,7 +349,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteCRDEndpoint,
 		Handler:  deleteCRDHandler,
 		Router:   r,
@@ -378,7 +379,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listReleasesEndpoint,
 		Handler:  listReleasesHandler,
 		Router:   r,
@@ -413,7 +414,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: streamPodLogsEndpoint,
 		Handler:  streamPodLogsHandler,
 		Router:   r,
@@ -447,7 +448,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: streamJobRunsEndpoint,
 		Handler:  streamJobRunsHandler,
 		Router:   r,
@@ -481,7 +482,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getPreviousLogsEndpoint,
 		Handler:  getPreviousLogsHandler,
 		Router:   r,
@@ -514,7 +515,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getJobPodsEndpoint,
 		Handler:  getJobPodsHandler,
 		Router:   r,
@@ -546,7 +547,7 @@ func getNamespaceRoutes(
 		config,
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteJobEndpoint,
 		Handler:  deleteJobHandler,
 		Router:   r,
@@ -578,7 +579,7 @@ func getNamespaceRoutes(
 		config,
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: stopJobEndpoint,
 		Handler:  stopJobHandler,
 		Router:   r,
@@ -611,7 +612,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getPodEndpoint,
 		Handler:  getPodHandler,
 		Router:   r,
@@ -643,7 +644,7 @@ func getNamespaceRoutes(
 		config,
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deletePodEndpoint,
 		Handler:  deletePodHandler,
 		Router:   r,
@@ -676,7 +677,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getPodEventsEndpoint,
 		Handler:  getPodEventsHandler,
 		Router:   r,
@@ -706,7 +707,7 @@ func getNamespaceRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getIngressEndpoint,
 		Handler:  getIngressHandler,
 		Router:   r,

+ 32 - 7
api/server/router/oauth_callback.go

@@ -5,11 +5,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/oauth_callback"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewOAuthCallbackRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewOAuthCallbackRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetOAuthCallbackRoutes,
 		Children:  children,
 	}
@@ -20,11 +21,11 @@ func GetOAuthCallbackRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	relPath := "/oauth"
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/oauth/slack/callback -> oauth_callback.NewOAuthCallbackSlackHandler
 	slackEndpoint := factory.NewAPIEndpoint(
@@ -44,7 +45,7 @@ func GetOAuthCallbackRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: slackEndpoint,
 		Handler:  slackHandler,
 		Router:   r,
@@ -68,11 +69,35 @@ func GetOAuthCallbackRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: doEndpoint,
 		Handler:  doHandler,
 		Router:   r,
 	})
 
+	// GET /api/oauth/gitlab/callback -> oauth_callback.NewOAuthCallbackGitlabHandler
+	gitlabEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/gitlab/callback",
+			},
+		},
+	)
+
+	gitlabHandler := oauth_callback.NewOAuthCallbackGitlabHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: gitlabEndpoint,
+		Handler:  gitlabHandler,
+		Router:   r,
+	})
+
 	return routes
 }

+ 247 - 41
api/server/router/project.go

@@ -4,20 +4,23 @@ import (
 	"fmt"
 
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/api_token"
 	"github.com/porter-dev/porter/api/server/handlers/billing"
 	"github.com/porter-dev/porter/api/server/handlers/cluster"
 	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/handlers/helmrepo"
 	"github.com/porter-dev/porter/api/server/handlers/infra"
+	"github.com/porter-dev/porter/api/server/handlers/policy"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/handlers/registry"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewProjectScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewProjectScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetProjectScopedRoutes,
 		Children:  children,
 	}
@@ -28,8 +31,8 @@ func GetProjectScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getProjectRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -50,7 +53,7 @@ func getProjectRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/projects/{project_id}"
 
 	newPath := &types.Path{
@@ -58,7 +61,7 @@ func getProjectRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id} -> project.NewProjectGetHandler
 	getEndpoint := factory.NewAPIEndpoint(
@@ -81,7 +84,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getEndpoint,
 		Handler:  getHandler,
 		Router:   r,
@@ -108,7 +111,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteEndpoint,
 		Handler:  deleteHandler,
 		Router:   r,
@@ -135,7 +138,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getPolicyEndpoint,
 		Handler:  getPolicyHandler,
 		Router:   r,
@@ -163,7 +166,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getOnboardingEndpoint,
 		Handler:  getOnboardingHandler,
 		Router:   r,
@@ -191,7 +194,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateOnboardingEndpoint,
 		Handler:  updateOnboardingHandler,
 		Router:   r,
@@ -218,7 +221,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getUsageEndpoint,
 		Handler:  getUsageHandler,
 		Router:   r,
@@ -245,7 +248,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getBillingEndpoint,
 		Handler:  getBillingHandler,
 		Router:   r,
@@ -274,7 +277,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getBillingTokenEndpoint,
 		Handler:  getBillingTokenHandler,
 		Router:   r,
@@ -298,7 +301,7 @@ func getProjectRoutes(
 		factory.GetDecoderValidator(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getBillingWebhookEndpoint,
 		Handler:  getBillingWebhookHandler,
 		Router:   r,
@@ -325,7 +328,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listClusterEndpoint,
 		Handler:  listClusterHandler,
 		Router:   r,
@@ -352,7 +355,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listGitReposEndpoint,
 		Handler:  listGitReposHandler,
 		Router:   r,
@@ -379,7 +382,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listCollaboratorsEndpoint,
 		Handler:  listCollaboratorsHandler,
 		Router:   r,
@@ -406,7 +409,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listRolesEndpoint,
 		Handler:  listRolesHandler,
 		Router:   r,
@@ -434,7 +437,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateRoleEndpoint,
 		Handler:  updateRoleHandler,
 		Router:   r,
@@ -462,7 +465,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteRoleEndpoint,
 		Handler:  deleteRoleHandler,
 		Router:   r,
@@ -489,7 +492,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listRegistriesEndpoint,
 		Handler:  listRegistriesHandler,
 		Router:   r,
@@ -517,7 +520,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createRegistryEndpoint,
 		Handler:  createRegistryHandler,
 		Router:   r,
@@ -545,7 +548,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getECRTokenEndpoint,
 		Handler:  getECRTokenHandler,
 		Router:   r,
@@ -573,7 +576,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getDOCRTokenEndpoint,
 		Handler:  getDOCRTokenHandler,
 		Router:   r,
@@ -601,7 +604,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getGCRTokenEndpoint,
 		Handler:  getGCRTokenHandler,
 		Router:   r,
@@ -629,7 +632,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getACRTokenEndpoint,
 		Handler:  getACRTokenHandler,
 		Router:   r,
@@ -657,7 +660,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getDockerhubTokenEndpoint,
 		Handler:  getDockerhubTokenHandler,
 		Router:   r,
@@ -685,7 +688,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createInfraEndpoint,
 		Handler:  createInfraHandler,
 		Router:   r,
@@ -712,7 +715,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getTemplatesEndpoint,
 		Handler:  getTemplatesHandler,
 		Router:   r,
@@ -739,7 +742,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getTemplateEndpoint,
 		Handler:  getTemplateHandler,
 		Router:   r,
@@ -767,7 +770,7 @@ func getProjectRoutes(
 	// 	factory.GetResultWriter(),
 	// )
 
-	// routes = append(routes, &Route{
+	// routes = append(routes, &router.Route{
 	// 	Endpoint: provisionECREndpoint,
 	// 	Handler:  provisionECRHandler,
 	// 	Router:   r,
@@ -797,7 +800,7 @@ func getProjectRoutes(
 	// 	factory.GetResultWriter(),
 	// )
 
-	// routes = append(routes, &Route{
+	// routes = append(routes, &router.Route{
 	// 	Endpoint: provisionEKSEndpoint,
 	// 	Handler:  provisionEKSHandler,
 	// 	Router:   r,
@@ -825,7 +828,7 @@ func getProjectRoutes(
 	// 	factory.GetResultWriter(),
 	// )
 
-	// routes = append(routes, &Route{
+	// routes = append(routes, &router.Route{
 	// 	Endpoint: provisionDOCREndpoint,
 	// 	Handler:  provisionDOCRHandler,
 	// 	Router:   r,
@@ -855,7 +858,7 @@ func getProjectRoutes(
 	// 	factory.GetResultWriter(),
 	// )
 
-	// routes = append(routes, &Route{
+	// routes = append(routes, &router.Route{
 	// 	Endpoint: provisionDOKSEndpoint,
 	// 	Handler:  provisionDOKSHandler,
 	// 	Router:   r,
@@ -883,7 +886,7 @@ func getProjectRoutes(
 	// 	factory.GetResultWriter(),
 	// )
 
-	// routes = append(routes, &Route{
+	// routes = append(routes, &router.Route{
 	// 	Endpoint: provisionGCREndpoint,
 	// 	Handler:  provisionGCRHandler,
 	// 	Router:   r,
@@ -913,12 +916,215 @@ func getProjectRoutes(
 	// 	factory.GetResultWriter(),
 	// )
 
-	// routes = append(routes, &Route{
+	// routes = append(routes, &router.Route{
 	// 	Endpoint: provisionGKEEndpoint,
 	// 	Handler:  provisionGKEHandler,
 	// 	Router:   r,
 	// })
 
+	//  POST /api/projects/{project_id}/policy -> policy.NewPolicyCreateHandler
+	policyCreateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/policy",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	policyCreateHandler := policy.NewPolicyCreateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: policyCreateEndpoint,
+		Handler:  policyCreateHandler,
+		Router:   r,
+	})
+
+	//  GET /api/projects/{project_id}/policies -> policy.NewPolicyListHandler
+	policyListEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/policies",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	policyListHandler := policy.NewPolicyListHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: policyListEndpoint,
+		Handler:  policyListHandler,
+		Router:   r,
+	})
+
+	//  GET /api/projects/{project_id}/policy/{policy_id} -> policy.NewPolicyGetHandler
+	policyGetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/policy/{%s}", relPath, types.URLParamPolicyID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	policyGetHandler := policy.NewPolicyGetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: policyGetEndpoint,
+		Handler:  policyGetHandler,
+		Router:   r,
+	})
+
+	//  POST /api/projects/{project_id}/api_token -> api_token.NewAPITokenCreateHandler
+	apiTokenCreateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/api_token",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	apiTokenCreateHandler := api_token.NewAPITokenCreateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: apiTokenCreateEndpoint,
+		Handler:  apiTokenCreateHandler,
+		Router:   r,
+	})
+
+	//  GET /api/projects/{project_id}/api_token -> api_token.NewAPITokenListHandler
+	apiTokenListEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/api_token", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	apiTokenListHandler := api_token.NewAPITokenListHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: apiTokenListEndpoint,
+		Handler:  apiTokenListHandler,
+		Router:   r,
+	})
+
+	//  GET /api/projects/{project_id}/api_token/{api_token_id} -> api_token.NewAPITokenGetHandler
+	apiTokenGetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/api_token/{%s}", relPath, types.URLParamTokenID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	apiTokenGetHandler := api_token.NewAPITokenGetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: apiTokenGetEndpoint,
+		Handler:  apiTokenGetHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/api_token/{api_token_id}/revoke -> api_token.NewAPITokenRevokeHandler
+	apiTokenRevokeEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/api_token/{%s}/revoke", relPath, types.URLParamTokenID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	apiTokenRevokeHandler := api_token.NewAPITokenRevokeHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: apiTokenRevokeEndpoint,
+		Handler:  apiTokenRevokeHandler,
+		Router:   r,
+	})
+
 	//  POST /api/projects/{project_id}/helmrepos -> helmrepo.NewHelmRepoCreateHandler
 	hrCreateEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
@@ -941,7 +1147,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: hrCreateEndpoint,
 		Handler:  hrCreateHandler,
 		Router:   r,
@@ -968,7 +1174,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: hrListEndpoint,
 		Handler:  hrListHandler,
 		Router:   r,
@@ -995,7 +1201,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getTagsEndpoint,
 		Handler:  getTagsHandler,
 		Router:   r,
@@ -1023,7 +1229,7 @@ func getProjectRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createTagEndpoint,
 		Handler:  createTagHandler,
 		Router:   r,

+ 258 - 16
api/server/router/project_integration.go

@@ -1,15 +1,18 @@
 package router
 
 import (
+	"fmt"
+
 	"github.com/go-chi/chi"
 	project_integration "github.com/porter-dev/porter/api/server/handlers/project_integration"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewProjectIntegrationScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewProjectIntegrationScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetProjectIntegrationScopedRoutes,
 		Children:  children,
 	}
@@ -20,8 +23,8 @@ func GetProjectIntegrationScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getProjectIntegrationRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -42,7 +45,7 @@ func getProjectIntegrationRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/integrations"
 
 	newPath := &types.Path{
@@ -50,7 +53,7 @@ func getProjectIntegrationRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/integrations/oauth -> project_integration.NewListOAuthHandler
 	listOAuthEndpoint := factory.NewAPIEndpoint(
@@ -73,7 +76,7 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listOAuthEndpoint,
 		Handler:  listOAuthHandler,
 		Router:   r,
@@ -100,7 +103,7 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listDOEndpoint,
 		Handler:  listDOHandler,
 		Router:   r,
@@ -128,7 +131,7 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createBasicEndpoint,
 		Handler:  createBasicHandler,
 		Router:   r,
@@ -156,7 +159,7 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createAWSEndpoint,
 		Handler:  createAWSHandler,
 		Router:   r,
@@ -183,7 +186,7 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listAWSEndpoint,
 		Handler:  listAWSHandler,
 		Router:   r,
@@ -211,7 +214,7 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: overwriteAWSEndpoint,
 		Handler:  overwriteAWSHandler,
 		Router:   r,
@@ -238,7 +241,7 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listAzureEndpoint,
 		Handler:  listAzureHandler,
 		Router:   r,
@@ -266,7 +269,7 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createGCPEndpoint,
 		Handler:  createGCPHandler,
 		Router:   r,
@@ -293,7 +296,7 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listGCPEndpoint,
 		Handler:  listGCPHandler,
 		Router:   r,
@@ -321,11 +324,250 @@ func getProjectIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createAzureEndpoint,
 		Handler:  createAzureHandler,
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/integrations/gitlab
+	listGitlabEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/gitlab",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listGitlabHandler := project_integration.NewListGitlabHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listGitlabEndpoint,
+		Handler:  listGitlabHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/integrations/gitlab
+	createGitlabEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/gitlab",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	createGitlabHandler := project_integration.NewCreateGitlabIntegration(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createGitlabEndpoint,
+		Handler:  createGitlabHandler,
+		Router:   r,
+	})
+
+	// PATCH /api/projects/{project_id}/integrations/gitlab/{integration_id}
+
+	// DELETE /api/projects/{project_id}/integrations/gitlab/{integration_id}
+
+	// GET /api/projects/{project_id}/integrations/git
+	listGitIntegrationsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/git",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listGitIntegrationsHandler := project_integration.NewListGitIntegrationHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listGitIntegrationsEndpoint,
+		Handler:  listGitIntegrationsHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos
+	listGitlabReposEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos", relPath, types.URLParamIntegrationID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.GitlabIntegrationScope,
+			},
+		},
+	)
+
+	listGitlabReposHandler := project_integration.NewListGitlabReposHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listGitlabReposEndpoint,
+		Handler:  listGitlabReposHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/{owner}/{name}/branches
+	listGitlabRepoBranchesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				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,
+			},
+		},
+	)
+
+	listGitlabRepoBranchesHandler := project_integration.NewListGitlabRepoBranchesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listGitlabRepoBranchesEndpoint,
+		Handler:  listGitlabRepoBranchesHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/{owner}/{name}/{branch}/contents
+	getGitlabRepoContentsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				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,
+			},
+		},
+	)
+
+	getGitlabRepoContentsHandler := project_integration.NewGetGitlabRepoContentsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getGitlabRepoContentsEndpoint,
+		Handler:  getGitlabRepoContentsHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/{owner}/{name}/{branch}/buildpack/detect
+	getGitlabRepoBuildpackEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				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,
+			},
+		},
+	)
+
+	getGitlabRepoBuildpackHandler := project_integration.NewGetGitlabRepoBuildpackHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getGitlabRepoBuildpackEndpoint,
+		Handler:  getGitlabRepoBuildpackHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/{owner}/{name}/{branch}/procfile
+	getGitlabRepoProcfileEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				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,
+			},
+		},
+	)
+
+	getGitlabRepoProcfileHandler := project_integration.NewGetGitlabRepoProcfileHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getGitlabRepoProcfileEndpoint,
+		Handler:  getGitlabRepoProcfileHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 37 - 8
api/server/router/project_oauth.go

@@ -6,11 +6,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/project_oauth"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewProjectOAuthScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewProjectOAuthScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetProjectOAuthScopedRoutes,
 		Children:  children,
 	}
@@ -21,8 +22,8 @@ func GetProjectOAuthScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getProjectOAuthRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -43,7 +44,7 @@ func getProjectOAuthRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/oauth"
 
 	newPath := &types.Path{
@@ -51,7 +52,7 @@ func getProjectOAuthRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/oauth/slack -> project_integration.NewProjectOAuthSlackHandler
 	slackEndpoint := factory.NewAPIEndpoint(
@@ -75,7 +76,7 @@ func getProjectOAuthRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: slackEndpoint,
 		Handler:  slackHandler,
 		Router:   r,
@@ -103,11 +104,39 @@ func getProjectOAuthRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: doEndpoint,
 		Handler:  doHandler,
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/oauth/gitlab -> project_integration.NewProjectOAuthGitlabHandler
+	gitlabEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/gitlab",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	gitlabHandler := project_oauth.NewProjectOAuthGitlabHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: gitlabEndpoint,
+		Handler:  gitlabHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 13 - 12
api/server/router/registry.go

@@ -7,11 +7,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/registry"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewRegistryScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewRegistryScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetRegistryScopedRoutes,
 		Children:  children,
 	}
@@ -22,8 +23,8 @@ func GetRegistryScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getRegistryRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -44,7 +45,7 @@ func getRegistryRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/registries/{registry_id}"
 
 	newPath := &types.Path{
@@ -52,7 +53,7 @@ func getRegistryRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/registries/{registry_id} -> registry.NewRegistryGetHandler
 	getEndpoint := factory.NewAPIEndpoint(
@@ -76,7 +77,7 @@ func getRegistryRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getEndpoint,
 		Handler:  getHandler,
 		Router:   r,
@@ -105,7 +106,7 @@ func getRegistryRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateEndpoint,
 		Handler:  updateHandler,
 		Router:   r,
@@ -133,7 +134,7 @@ func getRegistryRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteEndpoint,
 		Handler:  deleteHandler,
 		Router:   r,
@@ -161,7 +162,7 @@ func getRegistryRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listRepositoriesEndpoint,
 		Handler:  listRepositoriesHandler,
 		Router:   r,
@@ -193,7 +194,7 @@ func getRegistryRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listImagesEndpoint,
 		Handler:  listImagesHandler,
 		Router:   r,
@@ -222,7 +223,7 @@ func getRegistryRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createRepositoryEndpoint,
 		Handler:  createRepositoryHandler,
 		Router:   r,

+ 32 - 31
api/server/router/release.go

@@ -5,11 +5,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/release"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewReleaseScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewReleaseScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetReleaseScopedRoutes,
 		Children:  children,
 	}
@@ -20,8 +21,8 @@ func GetReleaseScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getReleaseRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -42,7 +43,7 @@ func getReleaseRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/releases/{name}/{version}"
 
 	newPath := &types.Path{
@@ -50,7 +51,7 @@ func getReleaseRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/{name}/{version} -> release.NewReleaseGetHandler
 	getEndpoint := factory.NewAPIEndpoint(
@@ -76,7 +77,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getEndpoint,
 		Handler:  getHandler,
 		Router:   r,
@@ -108,7 +109,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: streamFormEndpoint,
 		Handler:  streamFormHandler,
 		Router:   r,
@@ -138,7 +139,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getControllersEndpoint,
 		Handler:  getControllersHandler,
 		Router:   r,
@@ -168,7 +169,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getComponentsEndpoint,
 		Handler:  getComponentsHandler,
 		Router:   r,
@@ -198,7 +199,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getHistoryEndpoint,
 		Handler:  getHistoryHandler,
 		Router:   r,
@@ -228,7 +229,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getAllPodsEndpoint,
 		Handler:  getAllPodsHandler,
 		Router:   r,
@@ -258,7 +259,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateNotifsEndpoint,
 		Handler:  updateNotifsHandler,
 		Router:   r,
@@ -287,7 +288,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getNotifsEndpoint,
 		Handler:  getNotifsHandler,
 		Router:   r,
@@ -317,7 +318,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateBuildConfigEndpoint,
 		Handler:  updateBuildConfigHandler,
 		Router:   r,
@@ -346,7 +347,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getWebhookEndpoint,
 		Handler:  getWebhookHandler,
 		Router:   r,
@@ -376,7 +377,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createWebhookEndpoint,
 		Handler:  createWebhookHandler,
 		Router:   r,
@@ -406,7 +407,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getStepsEndpoint,
 		Handler:  getStepsHandler,
 		Router:   r,
@@ -436,7 +437,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateStepsEndpoint,
 		Handler:  updateStepsHandler,
 		Router:   r,
@@ -466,7 +467,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createReleaseEndpoint,
 		Handler:  createReleaseHandler,
 		Router:   r,
@@ -496,7 +497,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createAddonEndpoint,
 		Handler:  createAddonHandler,
 		Router:   r,
@@ -526,7 +527,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getGHATemplateEndpoint,
 		Handler:  getGHATemplateHandler,
 		Router:   r,
@@ -558,7 +559,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: rollbackEndpoint,
 		Handler:  rollbackHandler,
 		Router:   r,
@@ -590,7 +591,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: upgradeEndpoint,
 		Handler:  upgradeHandler,
 		Router:   r,
@@ -622,7 +623,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteEndpoint,
 		Handler:  deleteHandler,
 		Router:   r,
@@ -653,7 +654,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateImageBatchEndpoint,
 		Handler:  updateImageBatchHandler,
 		Router:   r,
@@ -685,7 +686,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getJobsEndpoint,
 		Handler:  getJobsHandler,
 		Router:   r,
@@ -716,7 +717,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getLatestJobRunEndpoint,
 		Handler:  getLatestJobRunHandler,
 		Router:   r,
@@ -747,7 +748,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getJobsStatusEndpoint,
 		Handler:  getJobsStatusHandler,
 		Router:   r,
@@ -776,7 +777,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createSubdomainEndpoint,
 		Handler:  createSubdomainHandler,
 		Router:   r,
@@ -808,7 +809,7 @@ func getReleaseRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: updateReleaseTagsEndpoint,
 		Handler:  updateReleaseTagsHandler,
 		Router:   r,

+ 44 - 22
api/server/router/router.go

@@ -12,8 +12,10 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz/policy"
 	"github.com/porter-dev/porter/api/server/router/middleware"
+	v1 "github.com/porter-dev/porter/api/server/router/v1"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -90,13 +92,13 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 			userRegisterer.Children...,
 		)
 
-		routes := [][]*Route{
+		routes := [][]*router.Route{
 			baseRoutes,
 			userRoutes,
 			oauthCallbackRoutes,
 		}
 
-		var allRoutes []*Route
+		var allRoutes []*router.Route
 		for _, r := range routes {
 			allRoutes = append(allRoutes, r...)
 		}
@@ -104,6 +106,39 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 		registerRoutes(config, allRoutes)
 	})
 
+	r.Route("/api/v1", func(r chi.Router) {
+		// set panic middleware for all API endpoints to catch panics
+		r.Use(panicMW.Middleware)
+
+		// set the content type for all API endpoints and log all request info
+		r.Use(middleware.ContentTypeJSON)
+
+		var allRoutes []*router.Route
+
+		v1RegistryRegisterer := v1.NewV1RegistryScopedRegisterer()
+		v1ReleaseRegisterer := v1.NewV1ReleaseScopedRegisterer()
+		v1NamespaceRegisterer := v1.NewV1NamespaceScopedRegisterer(v1ReleaseRegisterer)
+		v1ClusterRegisterer := v1.NewV1ClusterScopedRegisterer(v1NamespaceRegisterer)
+		v1ProjRegisterer := v1.NewV1ProjectScopedRegisterer(
+			v1ClusterRegisterer,
+			v1RegistryRegisterer,
+		)
+
+		v1Routes := v1ProjRegisterer.GetRoutes(
+			r,
+			config,
+			&types.Path{
+				RelativePath: "",
+			},
+			endpointFactory,
+			v1ProjRegisterer.Children...,
+		)
+
+		allRoutes = append(allRoutes, v1Routes...)
+
+		registerRoutes(config, allRoutes)
+	})
+
 	staticFilePath := config.ServerConf.StaticFilePath
 	fs := http.FileServer(http.Dir(staticFilePath))
 
@@ -128,25 +163,7 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 	return r
 }
 
-type Route struct {
-	Endpoint *shared.APIEndpoint
-	Handler  http.Handler
-	Router   chi.Router
-}
-
-type Registerer struct {
-	GetRoutes func(
-		r chi.Router,
-		config *config.Config,
-		basePath *types.Path,
-		factory shared.APIEndpointFactory,
-		children ...*Registerer,
-	) []*Route
-
-	Children []*Registerer
-}
-
-func registerRoutes(config *config.Config, routes []*Route) {
+func registerRoutes(config *config.Config, routes []*router.Route) {
 	// Create a new "user-scoped" factory which will create a new user-scoped request
 	// after authentication. Each subsequent http.Handler can lookup the user in context.
 	authNFactory := authn.NewAuthNFactory(config)
@@ -192,7 +209,7 @@ func registerRoutes(config *config.Config, routes []*Route) {
 	releaseFactory := authz.NewReleaseScopedFactory(config)
 
 	// Policy doc loader loads the policy documents for a specific project.
-	policyDocLoader := policy.NewBasicPolicyDocumentLoader(config.Repo.Project())
+	policyDocLoader := policy.NewBasicPolicyDocumentLoader(config.Repo.Project(), config.Repo.Policy())
 
 	// set up logging middleware to log information about the request
 	loggerMw := middleware.NewRequestLoggerMiddleware(config.Logger)
@@ -200,6 +217,9 @@ func registerRoutes(config *config.Config, routes []*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)
 
@@ -235,6 +255,8 @@ func registerRoutes(config *config.Config, routes []*Route) {
 				atomicGroup.Use(operationFactory.Middleware)
 			case types.ReleaseScope:
 				atomicGroup.Use(releaseFactory.Middleware)
+			case types.GitlabIntegrationScope:
+				atomicGroup.Use(gitlabIntFactory.Middleware)
 			}
 		}
 

+ 10 - 9
api/server/router/slack_integration.go

@@ -5,11 +5,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/slack_integration"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewSlackIntegrationScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewSlackIntegrationScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetSlackIntegrationScopedRoutes,
 		Children:  children,
 	}
@@ -20,8 +21,8 @@ func GetSlackIntegrationScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes, projPath := getSlackIntegrationRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
@@ -42,7 +43,7 @@ func getSlackIntegrationRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) ([]*Route, *types.Path) {
+) ([]*router.Route, *types.Path) {
 	relPath := "/slack_integrations"
 
 	newPath := &types.Path{
@@ -50,7 +51,7 @@ func getSlackIntegrationRoutes(
 		RelativePath: relPath,
 	}
 
-	routes := make([]*Route, 0)
+	routes := make([]*router.Route, 0)
 
 	// GET /api/projects/{project_id}/slack_integrations -> slack_integration.NewListHandler
 	listEndpoint := factory.NewAPIEndpoint(
@@ -73,7 +74,7 @@ func getSlackIntegrationRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listEndpoint,
 		Handler:  listHandler,
 		Router:   r,
@@ -97,7 +98,7 @@ func getSlackIntegrationRoutes(
 
 	existsHandler := slack_integration.NewSlackIntegrationExists(config)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: existsEndpoint,
 		Handler:  existsHandler,
 		Router:   r,
@@ -121,7 +122,7 @@ func getSlackIntegrationRoutes(
 
 	deleteHandler := slack_integration.NewSlackIntegrationDelete(config)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteEndpoint,
 		Handler:  deleteHandler,
 		Router:   r,

+ 23 - 22
api/server/router/user.go

@@ -10,11 +10,12 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/user"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewUserScopedRegisterer(children ...*Registerer) *Registerer {
-	return &Registerer{
+func NewUserScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
 		GetRoutes: GetUserScopedRoutes,
 		Children:  children,
 	}
@@ -25,8 +26,8 @@ func GetUserScopedRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-	children ...*Registerer,
-) []*Route {
+	children ...*router.Registerer,
+) []*router.Route {
 	routes := getUserRoutes(r, config, basePath, factory)
 
 	for _, child := range children {
@@ -45,8 +46,8 @@ func getUserRoutes(
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) []*Route {
-	routes := make([]*Route, 0)
+) []*router.Route {
+	routes := make([]*router.Route, 0)
 
 	// POST /api/welcome -> user.NewUserWelcomeHandler
 	welcomeEndpoint := factory.NewAPIEndpoint(
@@ -67,7 +68,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: welcomeEndpoint,
 		Handler:  welcomeHandler,
 		Router:   r,
@@ -93,7 +94,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: cliLoginUserEndpoint,
 		Handler:  cliLoginUserHandler,
 		Router:   r,
@@ -114,7 +115,7 @@ func getUserRoutes(
 
 	logoutUserHandler := user.NewUserLogoutHandler(config)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: logoutUserEndpoint,
 		Handler:  logoutUserHandler,
 		Router:   r,
@@ -138,7 +139,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: authCheckEndpoint,
 		Handler:  authCheckHandler,
 		Router:   r,
@@ -162,7 +163,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: deleteUserEndpoint,
 		Handler:  deleteUserHandler,
 		Router:   r,
@@ -187,7 +188,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: createEndpoint,
 		Handler:  createHandler,
 		Router:   r,
@@ -211,7 +212,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listEndpoint,
 		Handler:  listHandler,
 		Router:   r,
@@ -232,7 +233,7 @@ func getUserRoutes(
 
 	emailVerifyInitiateHandler := user.NewVerifyEmailInitiateHandler(config)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: emailVerifyInitiateEndpoint,
 		Handler:  emailVerifyInitiateHandler,
 		Router:   r,
@@ -257,7 +258,7 @@ func getUserRoutes(
 		factory.GetDecoderValidator(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: emailVerifyFinalizeEndpoint,
 		Handler:  emailVerifyFinalizeHandler,
 		Router:   r,
@@ -282,7 +283,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: listTemplatesEndpoint,
 		Handler:  listTemplatesRequest,
 		Router:   r,
@@ -311,7 +312,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getTemplateEndpoint,
 		Handler:  getTemplateRequest,
 		Router:   r,
@@ -340,7 +341,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: getTemplateUpgradeNotesEndpoint,
 		Handler:  getTemplateUpgradeNotesRequest,
 		Router:   r,
@@ -365,7 +366,7 @@ func getUserRoutes(
 		config,
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: githubAppOAuthStartEndpoint,
 		Handler:  githubAppOAuthStartHandler,
 		Router:   r,
@@ -390,7 +391,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: githubAppOAuthCallbackEndpoint,
 		Handler:  githubAppOAuthCallbackHandler,
 		Router:   r,
@@ -415,7 +416,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: githubAppAccountsEndpoint,
 		Handler:  githubAppAccountsHandler,
 		Router:   r,
@@ -440,7 +441,7 @@ func getUserRoutes(
 		factory.GetResultWriter(),
 	)
 
-	routes = append(routes, &Route{
+	routes = append(routes, &router.Route{
 		Endpoint: canCreateProjectEndpoint,
 		Handler:  canCreateProjectHandler,
 		Router:   r,

+ 172 - 0
api/server/router/v1/cluster.go

@@ -0,0 +1,172 @@
+package v1
+
+import (
+	"fmt"
+
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/cluster"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewV1ClusterScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetV1ClusterScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetV1ClusterScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getV1ClusterRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getV1ClusterRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/clusters/{cluster_id}"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	var routes []*router.Route
+
+	// POST /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces -> cluster.NewCreateNamespaceHandler
+	createNamespaceEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/namespaces",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	createNamespaceHandler := cluster.NewCreateNamespaceHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createNamespaceEndpoint,
+		Handler:  createNamespaceHandler,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace} -> cluster.NewGetNamespaceHandler
+	getNamespaceEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/namespaces/{%s}", relPath, types.URLParamNamespace),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getNamespaceHandler := cluster.NewGetNamespaceHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getNamespaceEndpoint,
+		Handler:  getNamespaceHandler,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces -> cluster.NewListNamespacesHandler
+	listNamespacesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/namespaces",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	listNamespacesHandler := cluster.NewListNamespacesHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listNamespacesEndpoint,
+		Handler:  listNamespacesHandler,
+		Router:   r,
+	})
+
+	// DELETE /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace} -> cluster.NewDeleteNamespaceHandler
+	deleteNamespaceEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/namespaces/{%s}", relPath, types.URLParamNamespace),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	deleteNamespaceHandler := cluster.NewDeleteNamespaceHandler(
+		config,
+		factory.GetDecoderValidator(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deleteNamespaceEndpoint,
+		Handler:  deleteNamespaceHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 56 - 0
api/server/router/v1/namespace.go

@@ -0,0 +1,56 @@
+package v1
+
+import (
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewV1NamespaceScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetV1NamespaceScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetV1NamespaceScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getV1NamespaceRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getV1NamespaceRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/namespaces/{namespace}"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	var routes []*router.Route
+
+	return routes, newPath
+}

+ 56 - 0
api/server/router/v1/project.go

@@ -0,0 +1,56 @@
+package v1
+
+import (
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewV1ProjectScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetV1ProjectScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetV1ProjectScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getV1ProjectRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getV1ProjectRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/projects/{project_id}"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	var routes []*router.Route
+
+	return routes, newPath
+}

+ 259 - 0
api/server/router/v1/registry.go

@@ -0,0 +1,259 @@
+package v1
+
+import (
+	"fmt"
+
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/registry"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewV1RegistryScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetV1RegistryScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetV1RegistryScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getV1RegistryRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getV1RegistryRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/registries"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	var routes []*router.Route
+
+	// POST /api/v1/projects/{project_id}/registries -> registry.NewRegistryCreateHandler
+	createRegistryEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	createRegistryHandler := registry.NewRegistryCreateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createRegistryEndpoint,
+		Handler:  createRegistryHandler,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/registries/{registry_id} -> registry.NewRegistryGetHandler
+	getEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{registry_id}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.RegistryScope,
+			},
+		},
+	)
+
+	getHandler := registry.NewRegistryGetHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getEndpoint,
+		Handler:  getHandler,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/registries -> registry.NewRegistryListHandler
+	listRegistriesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listRegistriesHandler := registry.NewRegistryListHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listRegistriesEndpoint,
+		Handler:  listRegistriesHandler,
+		Router:   r,
+	})
+
+	// DELETE /api/v1/projects/{project_id}/registries/{registry_id} -> registry.NewRegistryDeleteHandler
+	deleteEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{registry_id}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.RegistryScope,
+			},
+		},
+	)
+
+	deleteHandler := registry.NewRegistryDeleteHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deleteEndpoint,
+		Handler:  deleteHandler,
+		Router:   r,
+	})
+
+	// POST /api/v1/projects/{project_id}/registries/{registry_id}/repositories -> registry.NewRegistryCreateRepositoryHandler
+	createRepositoryEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{registry_id}/repositories",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.RegistryScope,
+			},
+		},
+	)
+
+	createRepositoryHandler := registry.NewRegistryCreateRepositoryHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createRepositoryEndpoint,
+		Handler:  createRepositoryHandler,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/registries/{registry_id}/repositories -> registry.NewRegistryListRepositoriesHandler
+	listRepositoriesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{registry_id}/repositories",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.RegistryScope,
+			},
+		},
+	)
+
+	listRepositoriesHandler := registry.NewRegistryListRepositoriesHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listRepositoriesEndpoint,
+		Handler:  listRepositoriesHandler,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/registries/{registry_id}/repositories/* -> registry.NewRegistryListImagesHandler
+	listImagesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{registry_id}/repositories/%s",
+					relPath,
+					types.URLParamWildcard,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.RegistryScope,
+			},
+		},
+	)
+
+	listImagesHandler := registry.NewRegistryListImagesHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listImagesEndpoint,
+		Handler:  listImagesHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 212 - 0
api/server/router/v1/release.go

@@ -0,0 +1,212 @@
+package v1
+
+import (
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/namespace"
+	"github.com/porter-dev/porter/api/server/handlers/release"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewV1ReleaseScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetV1ReleaseScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetV1ReleaseScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getV1ReleaseRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getV1ReleaseRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/releases"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	var routes []*router.Route
+
+	// POST /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases -> release.NewCreateReleaseHandler
+	createReleaseEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+			},
+		},
+	)
+
+	createReleaseHandler := release.NewCreateReleaseHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createReleaseEndpoint,
+		Handler:  createReleaseHandler,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/{name}/{version} -> release.NewReleaseGetHandler
+	getEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{name}/{version}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+				types.ReleaseScope,
+			},
+		},
+	)
+
+	getHandler := release.NewReleaseGetHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getEndpoint,
+		Handler:  getHandler,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases -> namespace.NewListReleasesHandler
+	listReleasesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+			},
+		},
+	)
+
+	listReleasesHandler := namespace.NewListReleasesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listReleasesEndpoint,
+		Handler:  listReleasesHandler,
+		Router:   r,
+	})
+
+	// PATCH /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/{name}/{version} ->
+	// release.NewUpgradeReleaseHandler
+	upgradeEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPatch,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{name}/{version}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+				types.ReleaseScope,
+			},
+		},
+	)
+
+	upgradeHandler := release.NewUpgradeReleaseHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: upgradeEndpoint,
+		Handler:  upgradeHandler,
+		Router:   r,
+	})
+
+	// DELETE /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/{name}/{version} ->
+	// release.NewDeleteReleaseHandler
+	deleteEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+				types.ReleaseScope,
+			},
+		},
+	)
+
+	deleteHandler := release.NewDeleteReleaseHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deleteEndpoint,
+		Handler:  deleteHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 25 - 0
api/server/shared/apierrors/errors.go

@@ -92,6 +92,31 @@ func (e *ErrPassThroughToClient) GetStatusCode() int {
 	return e.statusCode
 }
 
+// errors that denote that a resource was not found
+type ErrNotFound struct {
+	err error
+}
+
+func NewErrNotFound(err error) RequestError {
+	return &ErrNotFound{err}
+}
+
+func (e *ErrNotFound) Error() string {
+	return e.err.Error()
+}
+
+func (e *ErrNotFound) InternalError() string {
+	return e.err.Error()
+}
+
+func (e *ErrNotFound) ExternalError() string {
+	return "Resource not found."
+}
+
+func (e *ErrNotFound) GetStatusCode() int {
+	return http.StatusNotFound
+}
+
 type ErrorOpts struct {
 	Code uint
 }

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

+ 20 - 0
api/server/shared/commonutils/gitlab.go

@@ -0,0 +1,20 @@
+package commonutils
+
+import (
+	"github.com/porter-dev/porter/api/server/shared/config"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"golang.org/x/oauth2"
+)
+
+func GetGitlabOAuthConf(conf *config.Config, giIntegration *ints.GitlabIntegration) *oauth2.Config {
+	return &oauth2.Config{
+		ClientID:     string(giIntegration.AppClientID),
+		ClientSecret: string(giIntegration.AppClientSecret),
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  giIntegration.InstanceURL + "/oauth/authorize",
+			TokenURL: giIntegration.InstanceURL + "/oauth/token",
+		},
+		RedirectURL: conf.ServerConf.ServerURL + "/api/oauth/gitlab/callback",
+		Scopes:      []string{"api", "profile", "email"},
+	}
+}

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

+ 28 - 0
api/server/shared/router/router.go

@@ -0,0 +1,28 @@
+package router
+
+import (
+	"net/http"
+
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+)
+
+type Route struct {
+	Endpoint *shared.APIEndpoint
+	Handler  http.Handler
+	Router   chi.Router
+}
+
+type Registerer struct {
+	GetRoutes func(
+		r chi.Router,
+		config *config.Config,
+		basePath *types.Path,
+		factory shared.APIEndpointFactory,
+		children ...*Registerer,
+	) []*Route
+
+	Children []*Registerer
+}

+ 28 - 0
api/types/api_token.go

@@ -0,0 +1,28 @@
+package types
+
+import "time"
+
+const URLParamTokenID URLParam = "api_token_id"
+
+type APITokenMeta struct {
+	CreatedAt time.Time `json:"created_at"`
+	ExpiresAt time.Time `json:"expires_at"`
+
+	ID         string `json:"id"`
+	PolicyName string `json:"policy_name"`
+	PolicyUID  string `json:"policy_uid"`
+	Name       string `json:"name"`
+}
+
+type APIToken struct {
+	*APITokenMeta
+
+	Policy []*PolicyDocument `json:"policy"`
+	Token  string            `json:"token,omitempty"`
+}
+
+type CreateAPIToken struct {
+	PolicyUID string    `json:"policy_uid" form:"required"`
+	ExpiresAt time.Time `json:"expires_at"`
+	Name      string    `json:"name" form:"required"`
+}

+ 12 - 8
api/types/git_action_config.go

@@ -11,9 +11,12 @@ type GitActionConfig struct {
 	// The complete image repository uri to pull from
 	ImageRepoURI string `json:"image_repo_uri"`
 
-	// The git integration id
+	// The github integration ID
 	GitRepoID uint `json:"git_repo_id"`
 
+	// The gitlab integration ID
+	GitlabIntegrationID uint `json:"gitlab_integration_id"`
+
 	// The path to the dockerfile in the git repo
 	DockerfilePath string `json:"dockerfile_path"`
 
@@ -22,13 +25,14 @@ type GitActionConfig struct {
 }
 
 type CreateGitActionConfigRequest struct {
-	GitRepo        string `json:"git_repo" form:"required"`
-	GitBranch      string `json:"git_branch"`
-	ImageRepoURI   string `json:"image_repo_uri" form:"required"`
-	DockerfilePath string `json:"dockerfile_path"`
-	FolderPath     string `json:"folder_path"`
-	GitRepoID      uint   `json:"git_repo_id" form:"required"`
-	RegistryID     uint   `json:"registry_id"`
+	GitRepo             string `json:"git_repo" form:"required"`
+	GitBranch           string `json:"git_branch"`
+	ImageRepoURI        string `json:"image_repo_uri" form:"required"`
+	DockerfilePath      string `json:"dockerfile_path"`
+	FolderPath          string `json:"folder_path"`
+	GitRepoID           uint   `json:"git_repo_id"`
+	GitlabIntegrationID uint   `json:"gitlab_integration_id"`
+	RegistryID          uint   `json:"registry_id"`
 
 	ShouldCreateWorkflow bool `json:"should_create_workflow"`
 }

+ 1 - 0
api/types/infra.go

@@ -30,6 +30,7 @@ const (
 	InfraACR  InfraKind = "acr"
 
 	InfraRDS InfraKind = "rds"
+	InfraS3  InfraKind = "s3"
 )
 
 type Infra struct {

+ 101 - 12
api/types/policy.go

@@ -1,20 +1,23 @@
 package types
 
+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 {
@@ -57,6 +60,32 @@ var AdminPolicy = []*PolicyDocument{
 	{
 		Scope: ProjectScope,
 		Verbs: ReadWriteVerbGroup(),
+		Children: map[PermissionScope]*PolicyDocument{
+			ClusterScope: {
+				Scope: ClusterScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+			RegistryScope: {
+				Scope: RegistryScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+			HelmRepoScope: {
+				Scope: HelmRepoScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+			GitInstallationScope: {
+				Scope: GitInstallationScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+			InfraScope: {
+				Scope: InfraScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+			SettingsScope: {
+				Scope: SettingsScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+		},
 	},
 }
 
@@ -65,6 +94,26 @@ var DeveloperPolicy = []*PolicyDocument{
 		Scope: ProjectScope,
 		Verbs: ReadWriteVerbGroup(),
 		Children: map[PermissionScope]*PolicyDocument{
+			ClusterScope: {
+				Scope: ClusterScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+			RegistryScope: {
+				Scope: RegistryScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+			HelmRepoScope: {
+				Scope: HelmRepoScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+			GitInstallationScope: {
+				Scope: GitInstallationScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
+			InfraScope: {
+				Scope: InfraScope,
+				Verbs: ReadWriteVerbGroup(),
+			},
 			SettingsScope: {
 				Scope: SettingsScope,
 				Verbs: ReadVerbGroup(),
@@ -78,6 +127,26 @@ var ViewerPolicy = []*PolicyDocument{
 		Scope: ProjectScope,
 		Verbs: ReadVerbGroup(),
 		Children: map[PermissionScope]*PolicyDocument{
+			ClusterScope: {
+				Scope: ClusterScope,
+				Verbs: ReadVerbGroup(),
+			},
+			RegistryScope: {
+				Scope: RegistryScope,
+				Verbs: ReadVerbGroup(),
+			},
+			HelmRepoScope: {
+				Scope: HelmRepoScope,
+				Verbs: ReadVerbGroup(),
+			},
+			GitInstallationScope: {
+				Scope: GitInstallationScope,
+				Verbs: ReadVerbGroup(),
+			},
+			InfraScope: {
+				Scope: InfraScope,
+				Verbs: ReadVerbGroup(),
+			},
 			SettingsScope: {
 				Scope: SettingsScope,
 				Verbs: []APIVerb{},
@@ -85,3 +154,23 @@ var ViewerPolicy = []*PolicyDocument{
 		},
 	},
 }
+
+type CreatePolicy struct {
+	Name   string            `json:"name" form:"required"`
+	Policy []*PolicyDocument `json:"policy" form:"required"`
+}
+
+const URLParamPolicyID URLParam = "policy_id"
+
+type APIPolicyMeta struct {
+	CreatedAt time.Time `json:"created_at"`
+	UpdatedAt time.Time `json:"updated_at"`
+	ProjectID uint      `json:"project_id"`
+	UID       string    `json:"uid"`
+	Name      string    `json:"name"`
+}
+
+type APIPolicy struct {
+	*APIPolicyMeta
+	Policy []*PolicyDocument `json:"policy"`
+}

+ 1 - 0
api/types/project.go

@@ -7,6 +7,7 @@ type Project struct {
 	PreviewEnvsEnabled  bool    `json:"preview_envs_enabled"`
 	RDSDatabasesEnabled bool    `json:"enable_rds_databases"`
 	ManagedInfraEnabled bool    `json:"managed_infra_enabled"`
+	APITokensEnabled    bool    `json:"api_tokens_enabled"`
 }
 
 type CreateProjectRequest struct {

+ 34 - 0
api/types/project_integration.go

@@ -7,6 +7,7 @@ const (
 	OAuthGithub       OAuthIntegrationClient = "github"
 	OAuthDigitalOcean OAuthIntegrationClient = "do"
 	OAuthGoogle       OAuthIntegrationClient = "google"
+	OAuthGitlab       OAuthIntegrationClient = "gitlab"
 )
 
 // OAuthIntegrationClient is the name of an OAuth mechanism client
@@ -159,3 +160,36 @@ type CreateAzureResponse struct {
 }
 
 type ListAzureResponse []*AzureIntegration
+
+type GitlabIntegration struct {
+	CreatedAt time.Time `json:"created_at"`
+
+	ID uint `json:"id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	InstanceURL string `json:"instance_url"`
+}
+
+type ListGitlabResponse []*GitlabIntegration
+
+type CreateGitlabRequest struct {
+	InstanceURL     string `json:"instance_url"`
+	AppClientID     string `json:"client_id"`
+	AppClientSecret string `json:"client_secret"`
+}
+
+type CreateGitlabResponse struct {
+	*GitlabIntegration
+}
+
+type GitIntegration struct {
+	Provider       string `json:"provider" form:"required"`
+	Name           string `json:"name,omitempty"`
+	InstallationID int64  `json:"installation_id,omitempty"`
+	InstanceURL    string `json:"instance_url,omitempty"`
+	IntegrationID  uint   `json:"integration_id,omitempty"`
+}
+
+type ListGitIntegrationResponse []*GitIntegration

+ 5 - 5
api/types/release.go

@@ -45,11 +45,11 @@ type CreateReleaseBaseRequest struct {
 type CreateReleaseRequest struct {
 	*CreateReleaseBaseRequest
 
-	ImageURL           string                        `json:"image_url" form:"required"`
-	GithubActionConfig *CreateGitActionConfigRequest `json:"github_action_config,omitempty"`
-	BuildConfig        *CreateBuildConfigRequest     `json:"build_config,omitempty"`
-	Tags               []string                      `json:"tags,omitempty"`
-	SyncedEnvGroups    []string                      `json:"synced_env_groups,omitempty"`
+	ImageURL        string                        `json:"image_url" form:"required"`
+	GitActionConfig *CreateGitActionConfigRequest `json:"git_action_config,omitempty"`
+	BuildConfig     *CreateBuildConfigRequest     `json:"build_config,omitempty"`
+	Tags            []string                      `json:"tags,omitempty"`
+	SyncedEnvGroups []string                      `json:"synced_env_groups,omitempty"`
 }
 
 type CreateAddonRequest 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/deploy/create.go

@@ -135,7 +135,7 @@ func (c *CreateAgent) CreateFromGithub(
 				Name:            opts.ReleaseName,
 			},
 			ImageURL: imageURL,
-			GithubActionConfig: &types.CreateGitActionConfigRequest{
+			GitActionConfig: &types.CreateGitActionConfigRequest{
 				GitRepo:              ghOpts.Repo,
 				GitBranch:            ghOpts.Branch,
 				ImageRepoURI:         imageURL,

+ 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

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