瀏覽代碼

Merge branch 'master' of github.com:porter-dev/porter into nico/sidebar-replace-buttons-by-links

jnfrati 4 年之前
父節點
當前提交
a90ac73990
共有 100 個文件被更改,包括 3541 次插入2322 次删除
  1. 48 0
      .air.provisioner.toml
  2. 127 2
      .github/workflows/prerelease.yaml
  3. 3 0
      Makefile
  4. 16 0
      api/client/api.go
  5. 21 0
      api/client/k8s.go
  6. 20 0
      api/client/registry.go
  7. 1 1
      api/server/authn/handler.go
  8. 6 5
      api/server/authz/cluster.go
  9. 3 3
      api/server/authz/git_installation.go
  10. 2 2
      api/server/authz/helm_repo.go
  11. 2 2
      api/server/authz/infra.go
  12. 2 2
      api/server/authz/invite.go
  13. 60 0
      api/server/authz/operation.go
  14. 6 3
      api/server/authz/policy.go
  15. 2 2
      api/server/authz/project.go
  16. 2 2
      api/server/authz/registry.go
  17. 3 3
      api/server/authz/release.go
  18. 54 0
      api/server/handlers/database/update.go
  19. 3 3
      api/server/handlers/handler.go
  20. 76 0
      api/server/handlers/helmrepo/create.go
  21. 81 0
      api/server/handlers/helmrepo/get_chart.go
  22. 44 0
      api/server/handlers/helmrepo/list.go
  23. 63 0
      api/server/handlers/helmrepo/list_charts.go
  24. 302 0
      api/server/handlers/infra/create.go
  25. 32 294
      api/server/handlers/infra/delete.go
  26. 584 0
      api/server/handlers/infra/forms.go
  27. 29 1
      api/server/handlers/infra/get.go
  28. 0 51
      api/server/handlers/infra/get_current.go
  29. 0 51
      api/server/handlers/infra/get_desired.go
  30. 52 0
      api/server/handlers/infra/get_operation.go
  31. 43 0
      api/server/handlers/infra/get_operation_logs.go
  32. 41 0
      api/server/handlers/infra/get_state.go
  33. 83 0
      api/server/handlers/infra/get_template.go
  34. 10 3
      api/server/handlers/infra/list.go
  35. 44 0
      api/server/handlers/infra/list_operations.go
  36. 100 0
      api/server/handlers/infra/list_templates.go
  37. 0 195
      api/server/handlers/infra/retry.go
  38. 127 0
      api/server/handlers/infra/retry_create.go
  39. 73 0
      api/server/handlers/infra/retry_delete.go
  40. 87 12
      api/server/handlers/infra/stream_logs.go
  41. 116 0
      api/server/handlers/infra/stream_state.go
  42. 126 0
      api/server/handlers/infra/update.go
  43. 22 5
      api/server/handlers/namespace/clone_env_group.go
  44. 9 9
      api/server/handlers/namespace/get_env_group.go
  45. 56 0
      api/server/handlers/namespace/stream_job_runs.go
  46. 9 9
      api/server/handlers/project/create_test.go
  47. 1 1
      api/server/handlers/project/get_test.go
  48. 2 2
      api/server/handlers/project/list_test.go
  49. 0 70
      api/server/handlers/provision/helpers.go
  50. 0 137
      api/server/handlers/provision/provision_docr.go
  51. 0 138
      api/server/handlers/provision/provision_doks.go
  52. 0 136
      api/server/handlers/provision/provision_ecr.go
  53. 0 138
      api/server/handlers/provision/provision_eks.go
  54. 0 135
      api/server/handlers/provision/provision_gcr.go
  55. 0 139
      api/server/handlers/provision/provision_gke.go
  56. 0 336
      api/server/handlers/provision/provision_rds.go
  57. 2 2
      api/server/handlers/release/create.go
  58. 50 1
      api/server/handlers/release/create_addon.go
  59. 1 1
      api/server/handlers/release/get.go
  60. 18 2
      api/server/handlers/release/get_jobs.go
  61. 62 0
      api/server/handlers/release/get_latest_job_run.go
  62. 11 6
      api/server/handlers/release/ugprade.go
  63. 1 1
      api/server/handlers/template/get.go
  64. 1 1
      api/server/handlers/template/list.go
  65. 2 2
      api/server/handlers/user/cli_login.go
  66. 38 0
      api/server/handlers/user/create.go
  67. 12 12
      api/server/handlers/user/create_test.go
  68. 1 1
      api/server/handlers/user/current_test.go
  69. 2 2
      api/server/handlers/user/delete_test.go
  70. 1 1
      api/server/handlers/user/email_verify_test.go
  71. 6 0
      api/server/handlers/user/github_callback.go
  72. 6 0
      api/server/handlers/user/google_callback.go
  73. 12 12
      api/server/handlers/user/login_test.go
  74. 58 0
      api/server/router/helm_repo.go
  75. 204 24
      api/server/router/infra.go
  76. 1 1
      api/server/router/middleware/panic.go
  77. 4 2
      api/server/router/middleware/usage.go
  78. 2 2
      api/server/router/middleware/websocket.go
  79. 40 37
      api/server/router/namespace.go
  80. 214 74
      api/server/router/project.go
  81. 32 0
      api/server/router/release.go
  82. 6 0
      api/server/router/router.go
  83. 7 6
      api/server/shared/apierrors/errors.go
  84. 1 1
      api/server/shared/apitest/request.go
  85. 3 4
      api/server/shared/config/config.go
  86. 4 9
      api/server/shared/config/env/envconfs.go
  87. 12 19
      api/server/shared/config/loader/loader.go
  88. 4 3
      api/server/shared/config/metadata.go
  89. 2 2
      api/server/shared/endpoints.go
  90. 9 6
      api/server/shared/reader.go
  91. 10 5
      api/server/shared/writer.go
  92. 11 5
      api/types/database.go
  93. 1 0
      api/types/form.go
  94. 6 0
      api/types/helm_repo.go
  95. 72 0
      api/types/infra.go
  96. 9 0
      api/types/namespace.go
  97. 5 2
      api/types/policy.go
  98. 1 0
      api/types/project.go
  99. 2 184
      api/types/provision.go
  100. 2 0
      api/types/release.go

+ 48 - 0
.air.provisioner.toml

@@ -0,0 +1,48 @@
+# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
+
+# Working directory
+# . or absolute path, please note that the directories following must be under root.
+root = "."
+tmp_dir = "tmp"
+
+[build]
+# Just plain old shell command. You could use `make` as well.
+cmd = "go build -o ./tmp/provisioner -tags ee -ldflags=\"-X 'main.Version=dev-ee'\" ./cmd/provisioner"
+
+# Binary file yields from `cmd`.
+bin = "tmp/provisioner"
+# Customize binary.
+full_bin = "tmp/provisioner"
+# Watch these filename extensions.
+include_ext = ["go", "mod", "sum", "html"]
+# Ignore these filename extensions or directories.
+exclude_dir = ["tmp", "dashboard"]
+# Watch these directories if you specified.
+include_dir = []
+# Exclude files.
+exclude_file = []
+# This log file places in your tmp_dir.
+log = "air.log"
+# It's not necessary to trigger build each time file changes if it's too frequent.
+delay = 1000 # ms
+# Stop running old binary when build errors occur.
+stop_on_error = true
+# Send Interrupt signal before killing process (windows does not support this feature)
+send_interrupt = false
+# Delay after sending Interrupt signal
+kill_delay = 500 # ms
+
+[log]
+# Show log time
+time = false
+
+[color]
+# Customize each part's color. If no color found, use the raw app log.
+main = "magenta"
+watcher = "cyan"
+build = "yellow"
+runner = "green"
+
+[misc]
+# Delete tmp directory on exit
+clean_on_exit = true

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

@@ -250,7 +250,7 @@ jobs:
   release:
     name: Zip binaries, create release and upload assets
     runs-on: ubuntu-latest
-    needs: 
+    needs:
     - notarize
     - build-linux
     steps:
@@ -395,4 +395,129 @@ jobs:
             --build-arg VERSION=${{steps.tag_name.outputs.tag}}
       - name: Push
         run: |
-          docker push public.ecr.aws/o1j4x7p4/porter-cli:${{steps.tag_name.outputs.tag}}
+          docker push public.ecr.aws/o1j4x7p4/porter-cli:${{steps.tag_name.outputs.tag}}
+  update-porter-update-action:
+    name: Update porter-update-action
+    runs-on: ubuntu-latest
+    needs: build-push-docker-cli
+    steps:
+      - name: Get tag name
+        id: tag_name
+        run: |
+          tag=${GITHUB_TAG/refs\/tags\//}
+          echo ::set-output name=tag::$tag
+        env:
+          GITHUB_TAG: ${{ github.ref }}
+      - name: Push new branch with updated CLI
+        run: |
+          cd $GITHUB_WORKSPACE
+
+          git clone https://abelanger5:${{ secrets.PORTER_DEV_GITHUB_TOKEN }}@github.com/porter-dev/porter-update-action
+
+          cd porter-update-action
+
+          git checkout -B "${{steps.tag_name.outputs.tag}}"
+
+          cat >Dockerfile <<EOL
+          FROM public.ecr.aws/o1j4x7p4/porter-cli:${{steps.tag_name.outputs.tag}}
+
+          COPY entrypoint.sh /action/
+
+          ENTRYPOINT ["/action/entrypoint.sh"]
+          EOL
+
+          git config user.name "Update Bot"
+          git config user.email "support@porter.run"
+
+          git add .
+
+          git commit -m "Update to CLI version ${{steps.tag_name.outputs.tag}}"
+
+          git push --set-upstream origin ${{steps.tag_name.outputs.tag}} -f
+  update-porter-cli-action:
+    name: Update porter-cli-action
+    runs-on: ubuntu-latest
+    needs: build-push-docker-cli
+    steps:
+      - name: Get tag name
+        id: tag_name
+        run: |
+          tag=${GITHUB_TAG/refs\/tags\//}
+          echo ::set-output name=tag::$tag
+        env:
+          GITHUB_TAG: ${{ github.ref }}
+      - name: Push new branch with updated CLI
+        run: |
+          cd $GITHUB_WORKSPACE
+
+          git clone https://abelanger5:${{ secrets.PORTER_DEV_GITHUB_TOKEN }}@github.com/porter-dev/porter-cli-action
+
+          cd porter-cli-action
+
+          git checkout -B "${{steps.tag_name.outputs.tag}}"
+
+          cat >Dockerfile <<EOL
+          FROM public.ecr.aws/o1j4x7p4/porter-cli:${{steps.tag_name.outputs.tag}}
+
+          COPY entrypoint.sh /action/
+
+          ENTRYPOINT ["/action/entrypoint.sh"]
+          EOL
+
+          git config user.name "Update Bot"
+          git config user.email "support@porter.run"
+
+          git add .
+
+          git commit -m "Update to CLI version ${{steps.tag_name.outputs.tag}}"
+
+          git push --set-upstream origin ${{steps.tag_name.outputs.tag}} -f
+  update-new-release-tests:
+    name: Update new-release-tests
+    runs-on: ubuntu-latest
+    needs: [update-porter-update-action, update-porter-cli-action]
+    steps:
+      - name: Get tag name
+        id: tag_name
+        run: |
+          tag=${GITHUB_TAG/refs\/tags\//}
+          echo ::set-output name=tag::$tag
+        env:
+          GITHUB_TAG: ${{ github.ref }}
+      - name: Update new-release-tests
+        run: |
+          cd $GITHUB_WORKSPACE
+
+          git clone https://abelanger5:${{ secrets.PORTER_DEV_GITHUB_TOKEN }}@github.com/porter-dev/new-release-tests
+
+          cd new-release-tests/.github/workflows
+
+          sed -i 's/uses: porter-dev\/porter-update-action.*/uses: porter-dev\/porter-update-action@${{ steps.tag_name.outputs.tag }}/g' porter_test_pack_production.yml
+          sed -i 's/uses: porter-dev\/porter-cli-action.*/uses: porter-dev\/porter-cli-action@${{ steps.tag_name.outputs.tag }}/g' porter_test_pack_production.yml
+
+          sed -i 's/uses: porter-dev\/porter-update-action.*/uses: porter-dev\/porter-update-action@${{ steps.tag_name.outputs.tag }}/g' porter_test_docker_production.yml
+          sed -i 's/uses: porter-dev\/porter-cli-action.*/uses: porter-dev\/porter-cli-action@${{ steps.tag_name.outputs.tag }}/g' porter_test_docker_production.yml
+
+          cd ../..
+
+          git config user.name "Update Bot"
+          git config user.email "support@porter.run"
+
+          git add .
+
+          git diff --quiet --exit-code || git commit -m "Update to Porter GHA version ${{steps.tag_name.outputs.tag}}"
+
+          git push -f
+  run-new-release-tests-workflows:
+    name: Run new-release-tests Porter workflows
+    runs-on: ubuntu-latest
+    needs: update-new-release-tests
+    steps:
+      - name: Run porter_test_pack_production.yml workflow
+        run: gh workflow run porter_test_pack_production.yml --repo porter-dev/new-release-tests
+        env:
+          GITHUB_TOKEN: ${{ secrets.PORTER_DEV_GITHUB_TOKEN }}
+      - name: Run porter_test_docker_production.yml workflow
+        run: gh workflow run porter_test_docker_production.yml --repo porter-dev/new-release-tests
+        env:
+          GITHUB_TOKEN: ${{ secrets.PORTER_DEV_GITHUB_TOKEN }}

+ 3 - 0
Makefile

@@ -18,3 +18,6 @@ build-cli:
 
 build-cli-dev:
 	go build -tags cli -o $(BINDIR)/porter ./cli
+
+start-provisioner-dev: install setup-env-files
+	bash ./scripts/dev-environment/StartProvisionerServer.sh

+ 16 - 0
api/client/api.go

@@ -24,6 +24,8 @@ type Client struct {
 	Cookie         *http.Cookie
 	CookieFilePath string
 	Token          string
+
+	cfToken string
 }
 
 // NewClient constructs a new client based on a set of options
@@ -45,6 +47,11 @@ func NewClient(baseURL string, cookieFileName string) *Client {
 		client.Cookie = cookie
 	}
 
+	// look for a cloudflare access token specifically for Porter
+	if cfToken := os.Getenv("PORTER_CF_ACCESS_TOKEN"); cfToken != "" {
+		client.cfToken = cfToken
+	}
+
 	return client
 }
 
@@ -57,6 +64,11 @@ func NewClientWithToken(baseURL, token string) *Client {
 		},
 	}
 
+	// look for a cloudflare access token specifically for Porter
+	if cfToken := os.Getenv("PORTER_CF_ACCESS_TOKEN"); cfToken != "" {
+		client.cfToken = cfToken
+	}
+
 	return client
 }
 
@@ -191,6 +203,10 @@ func (c *Client) sendRequest(req *http.Request, v interface{}, useCookie bool) (
 		req.AddCookie(c.Cookie)
 	}
 
+	if c.cfToken != "" {
+		req.Header.Set("cf-access-token", c.cfToken)
+	}
+
 	res, err := c.HTTPClient.Do(req)
 
 	if err != nil {

+ 21 - 0
api/client/k8s.go

@@ -91,6 +91,27 @@ func (c *Client) GetEnvGroup(
 	return resp, err
 }
 
+func (c *Client) CloneEnvGroup(
+	ctx context.Context,
+	projectID, clusterID uint,
+	namespace string,
+	req *types.CloneEnvGroupRequest,
+) (*types.EnvGroup, error) {
+	resp := &types.EnvGroup{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/namespaces/%s/envgroup/clone",
+			projectID, clusterID,
+			namespace,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
 func (c *Client) GetRelease(
 	ctx context.Context,
 	projectID, clusterID uint,

+ 20 - 0
api/client/registry.go

@@ -27,6 +27,26 @@ func (c *Client) CreateRegistry(
 	return resp, err
 }
 
+// CreateRegistry creates a new registry integration
+func (c *Client) CreateHelmRepo(
+	ctx context.Context,
+	projectID uint,
+	req *types.CreateHelmRepoRequest,
+) (*types.HelmRepo, error) {
+	resp := &types.HelmRepo{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/helmrepos",
+			projectID,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
 // ListRegistries returns a list of registries for a project
 func (c *Client) ListRegistries(
 	ctx context.Context,

+ 1 - 1
api/server/authn/handler.go

@@ -153,7 +153,7 @@ func (authn *AuthN) nextWithUserID(w http.ResponseWriter, r *http.Request, userI
 func (authn *AuthN) sendForbiddenError(err error, w http.ResponseWriter, r *http.Request) {
 	reqErr := apierrors.NewErrForbidden(err)
 
-	apierrors.HandleAPIError(authn.config, w, r, reqErr, true)
+	apierrors.HandleAPIError(authn.config.Logger, authn.config.Alerter, w, r, reqErr, true)
 }
 
 var errInvalidToken = fmt.Errorf("authorization header exists, but token is not valid")

+ 6 - 5
api/server/authz/cluster.go

@@ -50,11 +50,11 @@ func (p *ClusterScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(
 				fmt.Errorf("cluster with id %d not found in project %d", clusterID, proj.ID),
 			), true)
 		} else {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		}
 
 		return
@@ -86,9 +86,10 @@ func NewOutOfClusterAgentGetter(config *config.Config) KubernetesAgentGetter {
 
 func (d *OutOfClusterAgentGetter) GetOutOfClusterConfig(cluster *models.Cluster) *kubernetes.OutOfClusterConfig {
 	return &kubernetes.OutOfClusterConfig{
-		Repo:              d.config.Repo,
-		DigitalOceanOAuth: d.config.DOConf,
-		Cluster:           cluster,
+		Repo:                      d.config.Repo,
+		DigitalOceanOAuth:         d.config.DOConf,
+		Cluster:                   cluster,
+		AllowInClusterConnections: d.config.ServerConf.InitInCluster,
 	}
 }
 

+ 3 - 3
api/server/authz/git_installation.go

@@ -48,15 +48,15 @@ func (p *GitInstallationScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *ht
 	gitInstallation, err := p.config.Repo.GithubAppInstallation().ReadGithubAppInstallationByInstallationID(gitInstallationID)
 
 	if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
-		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrForbidden(err), true)
+		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(err), true)
 		return
 	} else if err != nil {
-		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		return
 	}
 
 	if err := p.doesUserHaveGitInstallationAccess(user.GithubAppIntegrationID, gitInstallationID); err != nil {
-		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		return
 	}
 

+ 2 - 2
api/server/authz/helm_repo.go

@@ -43,11 +43,11 @@ func (p *HelmRepoScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(
 				fmt.Errorf("helm repo with id %d not found in project %d", helmRepoID, proj.ID),
 			), true)
 		} else {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		}
 
 		return

+ 2 - 2
api/server/authz/infra.go

@@ -43,11 +43,11 @@ func (p *InfraScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(
 				fmt.Errorf("infra with id %d not found in project %d", infraID, proj.ID),
 			), true)
 		} else {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		}
 
 		return

+ 2 - 2
api/server/authz/invite.go

@@ -43,11 +43,11 @@ func (p *InviteScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(
 				fmt.Errorf("invite with id %d not found in project %d", inviteID, proj.ID),
 			), true)
 		} else {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		}
 
 		return

+ 60 - 0
api/server/authz/operation.go

@@ -0,0 +1,60 @@
+package authz
+
+import (
+	"context"
+	"errors"
+	"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"
+	"gorm.io/gorm"
+)
+
+type OperationScopedFactory struct {
+	config *config.Config
+}
+
+func NewOperationScopedFactory(
+	config *config.Config,
+) *OperationScopedFactory {
+	return &OperationScopedFactory{config}
+}
+
+func (p *OperationScopedFactory) Middleware(next http.Handler) http.Handler {
+	return &OperationScopedMiddleware{next, p.config}
+}
+
+type OperationScopedMiddleware struct {
+	next   http.Handler
+	config *config.Config
+}
+
+func (p *OperationScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+
+	reqScopes, _ := r.Context().Value(types.RequestScopeCtxKey).(map[types.PermissionScope]*types.RequestAction)
+	operationID := reqScopes[types.OperationScope].Resource.Name
+
+	// look for matching operation for the infra
+	operation, err := p.config.Repo.Infra().ReadOperation(infra.ID, operationID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(err), true)
+			return
+		}
+
+		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
+		return
+	}
+
+	ctx := NewOperationContext(r.Context(), operation)
+	r = r.Clone(ctx)
+	p.next.ServeHTTP(w, r)
+}
+
+func NewOperationContext(ctx context.Context, operation *models.Operation) context.Context {
+	return context.WithValue(ctx, types.OperationScope, operation)
+}

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

@@ -43,7 +43,7 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	reqScopes, reqErr := getRequestActionForEndpoint(r, h.endpointMeta)
 
 	if reqErr != nil {
-		apierrors.HandleAPIError(h.config, w, r, reqErr, true)
+		apierrors.HandleAPIError(h.config.Logger, h.config.Alerter, w, r, reqErr, true)
 		return
 	}
 
@@ -54,7 +54,7 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	policyDocs, reqErr := h.loader.LoadPolicyDocuments(user.ID, projID)
 
 	if reqErr != nil {
-		apierrors.HandleAPIError(h.config, w, r, reqErr, true)
+		apierrors.HandleAPIError(h.config.Logger, h.config.Alerter, w, r, reqErr, true)
 		return
 	}
 
@@ -63,7 +63,8 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	if !hasAccess {
 		apierrors.HandleAPIError(
-			h.config,
+			h.config.Logger,
+			h.config.Alerter,
 			w,
 			r,
 			apierrors.NewErrForbidden(fmt.Errorf("policy forbids action for user %d in project %d", user.ID, projID)),
@@ -107,6 +108,8 @@ func getRequestActionForEndpoint(
 			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamGitInstallationID)
 		case types.InfraScope:
 			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamInfraID)
+		case types.OperationScope:
+			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamOperationID)
 		case types.NamespaceScope:
 			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamNamespace)
 		case types.ReleaseScope:

+ 2 - 2
api/server/authz/project.go

@@ -41,14 +41,14 @@ func (p *ProjectScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(
 				fmt.Errorf("project not found with id %d", projID),
 			), true)
 
 			return
 		}
 
-		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		return
 	}
 

+ 2 - 2
api/server/authz/registry.go

@@ -43,11 +43,11 @@ func (p *RegistryScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(
 				fmt.Errorf("registry with id %d not found in project %d", registryID, proj.ID),
 			), true)
 		} else {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		}
 
 		return

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

@@ -40,7 +40,7 @@ func (p *ReleaseScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	helmAgent, err := p.agentGetter.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
-		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		return
 	}
 
@@ -57,12 +57,12 @@ func (p *ReleaseScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		// ugly casing since at the time of this commit Helm doesn't have an errors package.
 		// so we rely on the Helm error containing "not found"
 		if strings.Contains(err.Error(), "not found") {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrPassThroughToClient(
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrPassThroughToClient(
 				fmt.Errorf("release not found"),
 				http.StatusNotFound,
 			), true)
 		} else {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
+			apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
 		}
 
 		return

+ 54 - 0
api/server/handlers/database/update.go

@@ -0,0 +1,54 @@
+package database
+
+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 DatabaseUpdateStatusHandler struct {
+	handlers.PorterHandlerReader
+}
+
+func NewDatabaseUpdateStatusHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+) *DatabaseUpdateStatusHandler {
+	return &DatabaseUpdateStatusHandler{
+		PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, nil),
+	}
+}
+
+func (p *DatabaseUpdateStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// read the project from context
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+
+	req := &types.UpdateDatabaseStatusRequest{}
+
+	if ok := p.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	// read all clusters for this project
+	db, err := p.Repo().Database().ReadDatabaseByInfraID(proj.ID, infra.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	db.Status = req.Status
+
+	db, err = p.Repo().Database().UpdateDatabase(db)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+}

+ 3 - 3
api/server/handlers/handler.go

@@ -58,11 +58,11 @@ func (d *DefaultPorterHandler) Repo() repository.Repository {
 }
 
 func (d *DefaultPorterHandler) HandleAPIError(w http.ResponseWriter, r *http.Request, err apierrors.RequestError) {
-	apierrors.HandleAPIError(d.Config(), w, r, err, true)
+	apierrors.HandleAPIError(d.Config().Logger, d.Config().Alerter, w, r, err, true)
 }
 
 func (d *DefaultPorterHandler) HandleAPIErrorNoWrite(w http.ResponseWriter, r *http.Request, err apierrors.RequestError) {
-	apierrors.HandleAPIError(d.Config(), w, r, err, false)
+	apierrors.HandleAPIError(d.Config().Logger, d.Config().Alerter, w, r, err, false)
 }
 
 func (d *DefaultPorterHandler) WriteResult(w http.ResponseWriter, r *http.Request, v interface{}) {
@@ -123,7 +123,7 @@ func NewUnavailable(config *config.Config, handlerID string) *Unavailable {
 }
 
 func (u *Unavailable) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	apierrors.HandleAPIError(u.config, w, r, apierrors.NewErrPassThroughToClient(
+	apierrors.HandleAPIError(u.config.Logger, u.config.Alerter, w, r, apierrors.NewErrPassThroughToClient(
 		fmt.Errorf("%s not available in community edition", u.handlerID),
 		http.StatusBadRequest,
 	), true, apierrors.ErrorOpts{

+ 76 - 0
api/server/handlers/helmrepo/create.go

@@ -0,0 +1,76 @@
+package helmrepo
+
+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/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type HelmRepoCreateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewHelmRepoCreateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *HelmRepoCreateHandler {
+	return &HelmRepoCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *HelmRepoCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.CreateHelmRepoRequest{}
+
+	ok := p.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	// if a basic integration is specified, verify that it exists in the project
+	if request.BasicIntegrationID != 0 {
+		_, err := p.Repo().BasicIntegration().ReadBasicIntegration(proj.ID, request.BasicIntegrationID)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				p.HandleAPIError(w, r, apierrors.NewErrForbidden(
+					fmt.Errorf("basic integration with id %d not found in project %d", request.BasicIntegrationID, proj.ID),
+				))
+
+				return
+			}
+
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	hr := &models.HelmRepo{
+		Name:                   request.Name,
+		ProjectID:              proj.ID,
+		RepoURL:                request.URL,
+		BasicAuthIntegrationID: request.BasicIntegrationID,
+	}
+
+	// handle write to the database
+	hr, err := p.Repo().HelmRepo().CreateHelmRepo(hr)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	p.WriteResult(w, r, hr.ToHelmRepoType())
+}

+ 81 - 0
api/server/handlers/helmrepo/get_chart.go

@@ -0,0 +1,81 @@
+package helmrepo
+
+import (
+	"net/http"
+	"strings"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"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/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/templater/parser"
+)
+
+type ChartGetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewChartGetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ChartGetHandler {
+	return &ChartGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (t *ChartGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	helmRepo, _ := r.Context().Value(types.HelmRepoScope).(*models.HelmRepo)
+
+	name, _ := requestutils.GetURLParamString(r, types.URLParamTemplateName)
+	version, _ := requestutils.GetURLParamString(r, types.URLParamTemplateVersion)
+
+	// if version passed as latest, pass empty string to loader to get latest
+	if version == "latest" {
+		version = ""
+	}
+
+	chart, err := release.LoadChart(t.Config(), &release.LoadAddonChartOpts{
+		ProjectID:       proj.ID,
+		RepoURL:         helmRepo.RepoURL,
+		TemplateName:    name,
+		TemplateVersion: version,
+	})
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	parserDef := &parser.ClientConfigDefault{
+		HelmChart: chart,
+	}
+
+	res := &types.GetTemplateResponse{
+		RepoURL: helmRepo.RepoURL,
+	}
+	res.Metadata = chart.Metadata
+	res.Values = chart.Values
+
+	for _, file := range chart.Files {
+		if strings.Contains(file.Name, "form.yaml") {
+			formYAML, err := parser.FormYAMLFromBytes(parserDef, file.Data, "declared", "")
+
+			if err != nil {
+				break
+			}
+
+			res.Form = formYAML
+		} else if strings.Contains(file.Name, "README.md") {
+			res.Markdown = string(file.Data)
+		}
+	}
+
+	t.WriteResult(w, r, res)
+}

+ 44 - 0
api/server/handlers/helmrepo/list.go

@@ -0,0 +1,44 @@
+package helmrepo
+
+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 HelmRepoListHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewHelmRepoListHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *HelmRepoListHandler {
+	return &HelmRepoListHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *HelmRepoListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	hrs, err := c.Repo().HelmRepo().ListHelmReposByProjectID(proj.ID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := make([]*types.HelmRepo, 0)
+
+	for _, hr := range hrs {
+		res = append(res, hr.ToHelmRepoType())
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 63 - 0
api/server/handlers/helmrepo/list_charts.go

@@ -0,0 +1,63 @@
+package helmrepo
+
+import (
+	"net/http"
+
+	"k8s.io/helm/pkg/repo"
+
+	"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/helm/loader"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ChartListHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewChartListHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ChartListHandler {
+	return &ChartListHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (t *ChartListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	helmRepo, _ := r.Context().Value(types.HelmRepoScope).(*models.HelmRepo)
+
+	var repoIndex *repo.IndexFile
+	var err error
+
+	if helmRepo.BasicAuthIntegrationID != 0 {
+		// read the basic integration id
+		basic, err := t.Repo().BasicIntegration().ReadBasicIntegration(proj.ID, helmRepo.BasicAuthIntegrationID)
+
+		if err != nil {
+			t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		repoIndex, err = loader.LoadRepoIndex(&loader.BasicAuthClient{
+			Username: string(basic.Username),
+			Password: string(basic.Password),
+		}, helmRepo.RepoURL)
+	} else {
+		repoIndex, err = loader.LoadRepoIndexPublic(helmRepo.RepoURL)
+	}
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	charts := loader.RepoIndexToPorterChartList(repoIndex, helmRepo.RepoURL)
+
+	t.WriteResult(w, r, charts)
+}

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

@@ -0,0 +1,302 @@
+package infra
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/mitchellh/mapstructure"
+	"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"
+	"gorm.io/gorm"
+
+	ptypes "github.com/porter-dev/porter/provisioner/types"
+)
+
+type InfraCreateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewInfraCreateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *InfraCreateHandler {
+	return &InfraCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *InfraCreateHandler) 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.CreateInfraRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	var cluster *models.Cluster
+	var err error
+
+	if req.ClusterID != 0 {
+		cluster, err = c.Repo().Cluster().ReadCluster(proj.ID, req.ClusterID)
+
+		if err != nil {
+			if err == gorm.ErrRecordNotFound {
+				c.HandleAPIError(w, r, apierrors.NewErrForbidden(
+					fmt.Errorf("cluster with id %d not found in project %d", req.ClusterID, proj.ID),
+				))
+			} else {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			}
+
+			return
+		}
+	}
+
+	suffix, err := encryption.GenerateRandomBytes(6)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	sourceLink, sourceVersion := getSourceLinkAndVersion(types.InfraKind(req.Kind))
+
+	// create the infra object
+	infra := &models.Infra{
+		Kind:            types.InfraKind(req.Kind),
+		APIVersion:      "v2",
+		ProjectID:       proj.ID,
+		Suffix:          suffix,
+		Status:          types.StatusCreating,
+		CreatedByUserID: user.ID,
+		SourceLink:      sourceLink,
+		SourceVersion:   sourceVersion,
+		ParentClusterID: req.ClusterID,
+	}
+
+	// verify the credentials
+	err = checkInfraCredentials(c.Config(), proj, infra, req.InfraCredentials)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	// call apply on the provisioner service
+	vals := req.Values
+
+	// if this is cluster-scoped and the kind is RDS, run the postrenderer
+	if req.ClusterID != 0 && req.Kind == "rds" {
+		var ok bool
+
+		pr := &InfraRDSPostrenderer{
+			config: c.Config(),
+		}
+
+		if vals, ok = pr.Run(w, r, &Opts{
+			Cluster: cluster,
+			Values:  req.Values,
+		}); !ok {
+			return
+		}
+	}
+
+	// handle write to the database
+	infra, err = c.Repo().Infra().CreateInfra(infra)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	resp, err := c.Config().ProvisionerClient.Apply(context.Background(), proj.ID, infra.ID, &ptypes.ApplyBaseRequest{
+		Kind:          req.Kind,
+		Values:        vals,
+		OperationKind: "create",
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, resp)
+}
+
+func checkInfraCredentials(config *config.Config, proj *models.Project, infra *models.Infra, req *types.InfraCredentials) error {
+	if req == nil {
+		return nil
+	}
+
+	if req.DOIntegrationID != 0 {
+		_, err := config.Repo.OAuthIntegration().ReadOAuthIntegration(proj.ID, req.DOIntegrationID)
+
+		if err != nil {
+			return fmt.Errorf("do integration id %d not found in project %d", req.DOIntegrationID, proj.ID)
+		}
+
+		infra.DOIntegrationID = req.DOIntegrationID
+		infra.AWSIntegrationID = 0
+		infra.GCPIntegrationID = 0
+	} else if req.AWSIntegrationID != 0 {
+		_, err := config.Repo.AWSIntegration().ReadAWSIntegration(proj.ID, req.AWSIntegrationID)
+
+		if err != nil {
+			return fmt.Errorf("aws integration id %d not found in project %d", req.AWSIntegrationID, proj.ID)
+		}
+
+		infra.DOIntegrationID = 0
+		infra.AWSIntegrationID = req.AWSIntegrationID
+		infra.GCPIntegrationID = 0
+	} else if req.GCPIntegrationID != 0 {
+		_, err := config.Repo.GCPIntegration().ReadGCPIntegration(proj.ID, req.GCPIntegrationID)
+
+		if err != nil {
+			return fmt.Errorf("gcp integration id %d not found in project %d", req.GCPIntegrationID, proj.ID)
+		}
+
+		infra.DOIntegrationID = 0
+		infra.AWSIntegrationID = 0
+		infra.GCPIntegrationID = req.GCPIntegrationID
+	}
+
+	if infra.DOIntegrationID == 0 && infra.AWSIntegrationID == 0 && infra.GCPIntegrationID == 0 {
+		return fmt.Errorf("at least one integration id must be set")
+	}
+
+	return nil
+}
+
+// getSourceLinkAndVersion returns the source link and version for the infrastructure. For now,
+// this is hardcoded
+func getSourceLinkAndVersion(kind types.InfraKind) (string, string) {
+	switch kind {
+	case types.InfraECR:
+		return "porter/aws/ecr", "v0.1.0"
+	case types.InfraEKS:
+		return "porter/aws/eks", "v0.1.0"
+	case types.InfraRDS:
+		return "porter/aws/rds", "v0.1.0"
+	case types.InfraGCR:
+		return "porter/gcp/gcr", "v0.1.0"
+	case types.InfraGKE:
+		return "porter/gcp/gke", "v0.1.0"
+	case types.InfraDOCR:
+		return "porter/do/docr", "v0.1.0"
+	case types.InfraDOKS:
+		return "porter/do/doks", "v0.1.0"
+	}
+
+	return "porter/test", "v0.1.0"
+}
+
+type InfraRDSPostrenderer struct {
+	config *config.Config
+}
+
+type Opts struct {
+	Cluster *models.Cluster
+	Values  map[string]interface{}
+}
+
+func (i *InfraRDSPostrenderer) Run(w http.ResponseWriter, r *http.Request, opts *Opts) (map[string]interface{}, bool) {
+	if opts.Cluster != nil {
+		proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+		values := opts.Values
+
+		// find the corresponding infra id
+		clusterInfra, err := i.config.Repo.Infra().ReadInfra(proj.ID, opts.Cluster.InfraID)
+
+		if err != nil {
+			apierrors.HandleAPIError(i.config.Logger, i.config.Alerter, w, r, apierrors.NewErrForbidden(fmt.Errorf("could not get cluster infra: %v", err)), true)
+			return nil, false
+		}
+
+		clusterInfraOperation, err := i.config.Repo.Infra().GetLatestOperation(clusterInfra)
+
+		// get the raw state for the cluster
+		rawState, err := i.config.ProvisionerClient.GetRawState(context.Background(), models.GetWorkspaceID(clusterInfra, clusterInfraOperation))
+
+		if err != nil {
+			apierrors.HandleAPIError(i.config.Logger, i.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
+			return nil, false
+		}
+
+		vpcID, subnetIDs, err := getVPCFromEKSTFState(rawState)
+
+		if err != nil {
+			return values, false
+		}
+
+		// if the length of the subnets is not 3, return an error
+		if len(subnetIDs) < 3 {
+			apierrors.HandleAPIError(i.config.Logger, i.config.Alerter, w, r, apierrors.NewErrInternal(fmt.Errorf("invalid number of subnet IDs in VPC configuration")), true)
+			return nil, false
+		}
+
+		values["porter_cluster_vpc"] = vpcID
+		values["porter_cluster_subnet_1"] = subnetIDs[0]
+		values["porter_cluster_subnet_2"] = subnetIDs[1]
+		values["porter_cluster_subnet_3"] = subnetIDs[2]
+
+		return values, true
+	}
+
+	return opts.Values, true
+}
+
+type AWSVPCConfig struct {
+	SubNetIDs []string `json:"subnet_ids" mapstructure:"subnet_ids"`
+	VPCID     string   `json:"vpc_id" mapstructure:"vpc_id"`
+}
+
+func getVPCFromEKSTFState(tfState *ptypes.ParseableRawTFState) (string, []string, error) {
+	for _, resource := range tfState.Resources {
+		if "aws_eks_cluster.cluster" == resource.Type+"."+resource.Name {
+			for _, instance := range resource.Instances {
+				vpcConfig, ok := instance.Attributes["vpc_config"]
+				if !ok {
+					return "", []string{}, errors.New("name not found for the requested resource name-type")
+				}
+
+				awsVPCConfigIface, ok := vpcConfig.([]interface{})
+				if !ok {
+					fmt.Printf("%#v\n", vpcConfig)
+					return "", []string{}, errors.New("cannot cast returned value to vpc config")
+				}
+
+				if len(awsVPCConfigIface) == 0 {
+					return "", []string{}, errors.New("empty vpc config")
+				}
+
+				awsVPCConfigMap, ok := awsVPCConfigIface[0].(map[string]interface{})
+				if !ok {
+					return "", []string{}, errors.New("cannot cast returned value to vpc config map")
+				}
+
+				var awsVPCConfig AWSVPCConfig
+
+				err := mapstructure.Decode(awsVPCConfigMap, &awsVPCConfig)
+				if err != nil {
+					return "", []string{}, errors.New("cannot cast returned value to vpc config")
+				}
+
+				return awsVPCConfig.VPCID, awsVPCConfig.SubNetIDs, nil
+			}
+
+			return "", []string{}, errors.New("name not found for the requested resource name-type")
+		}
+	}
+
+	return "", []string{}, errors.New("name not found for the requested resource name-type")
+}

+ 32 - 294
api/server/handlers/infra/delete.go

@@ -1,24 +1,18 @@
 package infra
 
 import (
-	"encoding/json"
+	"context"
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/provision"
 	"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/analytics"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/ecr"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/eks"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/rds"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/do/docr"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/do/doks"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp/gke"
 	"github.com/porter-dev/porter/internal/models"
+
+	ptypes "github.com/porter-dev/porter/provisioner/types"
 )
 
 type InfraDeleteHandler struct {
@@ -36,315 +30,59 @@ func NewInfraDeleteHandler(
 }
 
 func (c *InfraDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
 
-	if infra.Kind == types.InfraDOKS || infra.Kind == types.InfraGKE || infra.Kind == types.InfraEKS {
-		c.Config().AnalyticsClient.Track(analytics.ClusterDestroyingStartTrack(
-			&analytics.ClusterDestroyingStartTrackOpts{
-				ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(infra.CreatedByUserID, infra.ProjectID, 0),
-				ClusterType:            infra.Kind,
-				InfraID:                infra.ID,
-			},
-		))
-	}
-
-	infra.Status = types.StatusDestroying
-	infra, err := c.Repo().Infra().UpdateInfra(infra)
+	req := &types.DeleteInfraRequest{}
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	if ok := c.DecodeAndValidate(w, r, req); !ok {
 		return
 	}
 
-	switch infra.Kind {
-	case types.InfraECR:
-		err = destroyECR(c.Config(), infra)
-	case types.InfraEKS:
-		err = destroyEKS(c.Config(), infra)
-	case types.InfraDOCR:
-		err = destroyDOCR(c.Config(), infra)
-	case types.InfraDOKS:
-		err = destroyDOKS(c.Config(), infra)
-	case types.InfraGKE:
-		err = destroyGKE(c.Config(), infra)
-	case types.InfraRDS:
-		err = destroyRDS(c.Config(), infra)
-	}
+	// verify the credentials
+	err := checkInfraCredentials(c.Config(), proj, infra, req.InfraCredentials)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		return
 	}
-}
-
-func destroyECR(conf *config.Config, infra *models.Infra) error {
-	lastAppliedECR := &types.CreateECRInfraRequest{}
-
-	// parse infra last applied into ECR config
-	if err := json.Unmarshal(infra.LastApplied, lastAppliedECR); err != nil {
-		return err
-	}
-
-	awsInt, err := conf.Repo.AWSIntegration().ReadAWSIntegration(infra.ProjectID, infra.AWSIntegrationID)
-
-	if err != nil {
-		return err
-	}
-
-	opts, err := provision.GetSharedProvisionerOpts(conf, infra)
-
-	vaultToken := ""
-
-	if conf.CredentialBackend != nil {
-		vaultToken, err = conf.CredentialBackend.CreateAWSToken(awsInt)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-
-	opts.ECR = &ecr.Conf{
-		AWSRegion: awsInt.AWSRegion,
-		ECRName:   lastAppliedECR.ECRName,
-	}
-
-	opts.OperationKind = provisioner.Destroy
-
-	err = conf.ProvisionerAgent.Provision(opts)
-
-	return err
-}
-
-func destroyEKS(conf *config.Config, infra *models.Infra) error {
-	lastAppliedEKS := &types.CreateEKSInfraRequest{}
-
-	// parse infra last applied into EKS config
-	if err := json.Unmarshal(infra.LastApplied, lastAppliedEKS); err != nil {
-		return err
-	}
-
-	awsInt, err := conf.Repo.AWSIntegration().ReadAWSIntegration(infra.ProjectID, infra.AWSIntegrationID)
-
-	if err != nil {
-		return err
-	}
-
-	opts, err := provision.GetSharedProvisionerOpts(conf, infra)
-
-	vaultToken := ""
-
-	if conf.CredentialBackend != nil {
-		vaultToken, err = conf.CredentialBackend.CreateAWSToken(awsInt)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-
-	opts.EKS = &eks.Conf{
-		AWSRegion:   awsInt.AWSRegion,
-		ClusterName: lastAppliedEKS.EKSName,
-		MachineType: lastAppliedEKS.MachineType,
-		IssuerEmail: lastAppliedEKS.IssuerEmail,
-	}
-	opts.OperationKind = provisioner.Destroy
-
-	err = conf.ProvisionerAgent.Provision(opts)
-
-	return err
-}
-
-func destroyRDS(conf *config.Config, infra *models.Infra) error {
-	// find the database and mark as deleting
-	database, err := conf.Repo.Database().ReadDatabaseByInfraID(infra.ProjectID, infra.ID)
-
-	if err != nil {
-		return err
-	}
-
-	database.Status = "destroying"
-
-	database, err = conf.Repo.Database().UpdateDatabase(database)
-
-	if err != nil {
-		return err
-	}
-
-	lastAppliedRDS := &types.RDSInfraLastApplied{}
-
-	// parse infra last applied into EKS config
-	if err := json.Unmarshal(infra.LastApplied, lastAppliedRDS); err != nil {
-		return err
-	}
-
-	awsInt, err := conf.Repo.AWSIntegration().ReadAWSIntegration(infra.ProjectID, infra.AWSIntegrationID)
-
-	if err != nil {
-		return err
-	}
-
-	opts, err := provision.GetSharedProvisionerOpts(conf, infra)
-
-	vaultToken := ""
-
-	if conf.CredentialBackend != nil {
-		vaultToken, err = conf.CredentialBackend.CreateAWSToken(awsInt)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-
-	opts.RDS = &rds.Conf{
-		AWSRegion:             awsInt.AWSRegion,
-		DBName:                lastAppliedRDS.DBName,
-		MachineType:           lastAppliedRDS.MachineType,
-		DBEngineVersion:       lastAppliedRDS.DBEngineVersion,
-		DBFamily:              lastAppliedRDS.DBFamily,
-		DBMajorEngineVersion:  lastAppliedRDS.DBMajorEngineVersion,
-		DBAllocatedStorage:    lastAppliedRDS.DBStorage,
-		DBMaxAllocatedStorage: lastAppliedRDS.DBMaxStorage,
-		DBStorageEncrypted:    lastAppliedRDS.DBStorageEncrypted,
-		Username:              lastAppliedRDS.Username,
-		Password:              lastAppliedRDS.Password,
-		VPCID:                 lastAppliedRDS.VPCID,
-		Subnet1:               lastAppliedRDS.Subnet1,
-		Subnet2:               lastAppliedRDS.Subnet2,
-		Subnet3:               lastAppliedRDS.Subnet3,
-		DeletionProtection:    lastAppliedRDS.DeletionProtection,
-	}
-
-	opts.OperationKind = provisioner.Destroy
-
-	err = conf.ProvisionerAgent.Provision(opts)
 
-	return err
-}
-
-func destroyDOCR(conf *config.Config, infra *models.Infra) error {
-	lastAppliedDOCR := &types.CreateDOCRInfraRequest{}
-
-	// parse infra last applied into DOCR config
-	if err := json.Unmarshal(infra.LastApplied, lastAppliedDOCR); err != nil {
-		return err
-	}
-
-	doInt, err := conf.Repo.OAuthIntegration().ReadOAuthIntegration(infra.ProjectID, infra.DOIntegrationID)
+	lastOperation, err := c.Repo().Infra().GetLatestOperation(infra)
 
 	if err != nil {
-		return err
-	}
-
-	opts, err := provision.GetSharedProvisionerOpts(conf, infra)
-
-	vaultToken := ""
-
-	if conf.CredentialBackend != nil {
-		vaultToken, err = conf.CredentialBackend.CreateOAuthToken(doInt)
-
-		if err != nil {
-			return err
-		}
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
 	}
 
-	opts.CredentialExchange.VaultToken = vaultToken
+	// if the last operation is in a "starting" state, block apply
+	if lastOperation.Status == "starting" {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("Operation currently in progress. Please try again when latest operation has completed."),
+			http.StatusBadRequest,
+		))
 
-	opts.DOCR = &docr.Conf{
-		DOCRName:             lastAppliedDOCR.DOCRName,
-		DOCRSubscriptionTier: lastAppliedDOCR.DOCRSubscriptionTier,
+		return
 	}
 
-	opts.OperationKind = provisioner.Destroy
-
-	err = conf.ProvisionerAgent.Provision(opts)
-
-	return err
-}
-
-func destroyDOKS(conf *config.Config, infra *models.Infra) error {
-	lastAppliedDOKS := &types.CreateDOKSInfraRequest{}
-
-	// parse infra last applied into DOKS config
-	if err := json.Unmarshal(infra.LastApplied, lastAppliedDOKS); err != nil {
-		return err
-	}
+	// mark the infra as destroying
+	infra.Status = types.StatusDestroying
 
-	doInt, err := conf.Repo.OAuthIntegration().ReadOAuthIntegration(infra.ProjectID, infra.DOIntegrationID)
+	infra, err = c.Repo().Infra().UpdateInfra(infra)
 
 	if err != nil {
-		return err
-	}
-
-	opts, err := provision.GetSharedProvisionerOpts(conf, infra)
-
-	vaultToken := ""
-
-	if conf.CredentialBackend != nil {
-		vaultToken, err = conf.CredentialBackend.CreateOAuthToken(doInt)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-
-	opts.DOKS = &doks.Conf{
-		DORegion:        lastAppliedDOKS.DORegion,
-		DOKSClusterName: lastAppliedDOKS.DOKSName,
-		IssuerEmail:     lastAppliedDOKS.IssuerEmail,
-	}
-
-	opts.OperationKind = provisioner.Destroy
-
-	err = conf.ProvisionerAgent.Provision(opts)
-
-	return err
-}
-
-func destroyGKE(conf *config.Config, infra *models.Infra) error {
-	lastAppliedGKE := &types.CreateGKEInfraRequest{}
-
-	// parse infra last applied into DOKS config
-	if err := json.Unmarshal(infra.LastApplied, lastAppliedGKE); err != nil {
-		return err
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
 	}
 
-	gcpInt, err := conf.Repo.GCPIntegration().ReadGCPIntegration(infra.ProjectID, infra.GCPIntegrationID)
+	// call apply on the provisioner service
+	resp, err := c.Config().ProvisionerClient.Delete(context.Background(), proj.ID, infra.ID, &ptypes.DeleteBaseRequest{
+		OperationKind: "delete",
+	})
 
 	if err != nil {
-		return err
-	}
-
-	opts, err := provision.GetSharedProvisionerOpts(conf, infra)
-
-	vaultToken := ""
-
-	if conf.CredentialBackend != nil {
-		vaultToken, err = conf.CredentialBackend.CreateGCPToken(gcpInt)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-	opts.GKE = &gke.Conf{
-		GCPProjectID: gcpInt.GCPProjectID,
-		GCPRegion:    lastAppliedGKE.GCPRegion,
-		ClusterName:  lastAppliedGKE.GKEName,
-		IssuerEmail:  lastAppliedGKE.IssuerEmail,
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
 	}
 
-	opts.OperationKind = provisioner.Destroy
-
-	err = conf.ProvisionerAgent.Provision(opts)
-
-	return err
+	c.WriteResult(w, r, resp)
 }

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

@@ -0,0 +1,584 @@
+package infra
+
+const testForm = `name: Test
+hasSource: false
+includeHiddenFields: true
+isClusterScoped: true
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: String to echo
+    - type: string-input
+      variable: echo
+      settings:
+        default: hello
+`
+
+const rdsForm = `name: RDS
+hasSource: false
+includeHiddenFields: true
+isClusterScoped: true
+tabs:
+- name: main
+  label: Main
+  sections:
+  - name: heading
+    contents: 
+    - type: heading
+      label: Database Settings
+  - name: user
+    contents:
+    - type: string-input
+      label: Database Master User
+      required: true
+      placeholder: "admin"
+      variable: db_user
+  - name: password
+    contents:
+    - type: string-input
+      required: true
+      label: Database Master Password
+      variable: db_passwd
+  - name: name
+    contents:
+    - type: string-input
+      label: Database Name
+      required: true
+      placeholder: "rds-staging"
+      variable: db_name
+  - name: machine-type
+    contents:
+    - type: select
+      label: ⚙️ Database Machine Type
+      variable: machine_type
+      settings:
+        default: db.t3.medium
+        options:
+        - label: db.t2.medium
+          value: db.t2.medium
+        - label: db.t2.xlarge
+          value: db.t2.xlarge
+        - label: db.t2.2xlarge
+          value: db.t2.2xlarge
+        - label: db.t3.medium
+          value: db.t3.medium
+        - label: db.t3.xlarge
+          value: db.t3.xlarge
+        - label: db.t3.2xlarge
+          value: db.t3.2xlarge
+  - name: family-versions
+    contents:
+    - type: select
+      label:  Database Family Version
+      variable: db_family
+      settings:
+        default: postgres13
+        options:
+        - label: "Postgres 9"
+          value: postgres9
+        - label: "Postgres 10"
+          value: postgres10
+        - label: "Postgres 11"
+          value: postgres11
+        - label: "Postgres 12"
+          value: postgres12
+        - label: "Postgres 13"
+          value: postgres13
+  - name: pg-9-versions
+    show_if: 
+      is: "postgres9"
+      variable: db_family
+    contents:
+    - type: select
+      label:  Database Version
+      variable: db_engine_version
+      settings:
+        default: "9.6.23"
+        options:
+        - label: "v9.6.1"
+          value: "9.6.1"
+        - label: "v9.6.2"
+          value: "9.6.2"
+        - label: "v9.6.3"
+          value: "9.6.3"
+        - label: "v9.6.4"
+          value: "9.6.4"
+        - label: "v9.6.5"
+          value: "9.6.5"
+        - label: "v9.6.6"
+          value: "9.6.6"
+        - label: "v9.6.7"
+          value: "9.6.7"
+        - label: "v9.6.8"
+          value: "9.6.8"
+        - label: "v9.6.10"
+          value: "9.6.10"
+        - label: "v9.6.11"
+          value: "9.6.11"
+        - label: "v9.6.12"
+          value: "9.6.12"
+        - label: "v9.6.13"
+          value: "9.6.13"
+        - label: "v9.6.14"
+          value: "9.6.14"
+        - label: "v9.6.15"
+          value: "9.6.15"
+        - label: "v9.6.16"
+          value: "9.6.16"
+        - label: "v9.6.17"
+          value: "9.6.17"
+        - label: "v9.6.18"
+          value: "9.6.18"
+        - label: "v9.6.19"
+          value: "9.6.19"
+        - label: "v9.6.20"
+          value: "9.6.20"
+        - label: "v9.6.21"
+          value: "9.6.21"
+        - label: "v9.6.22"
+          value: "9.6.22"
+        - label: "v9.6.23"
+          value: "9.6.23"
+  - name: pg-10-versions
+    show_if: 
+      is: "postgres10"
+      variable: db_family
+    contents:
+    - type: select
+      label:  Database Version
+      variable: db_engine_version
+      settings:
+        default: "10.18"
+        options:
+        - label: "v10.1"
+          value: "10.1"
+        - label: "v10.2"
+          value: "10.2"
+        - label: "v10.3"
+          value: "10.3"
+        - label: "v10.4"
+          value: "10.4"
+        - label: "v10.5"
+          value: "10.5"
+        - label: "v10.6"
+          value: "10.6"
+        - label: "v10.7"
+          value: "10.7"
+        - label: "v10.8"
+          value: "10.8"
+        - label: "v10.9"
+          value: "10.9"
+        - label: "v10.10"
+          value: "10.10"
+        - label: "v10.11"
+          value: "10.11"
+        - label: "v10.12"
+          value: "10.12"
+        - label: "v10.13"
+          value: "10.13"
+        - label: "v10.14"
+          value: "10.14"
+        - label: "v10.15"
+          value: "10.15"
+        - label: "v10.16"
+          value: "10.16"
+        - label: "v10.17"
+          value: "10.17"
+        - label: "v10.18"
+          value: "10.18"
+  - name: pg-11-versions
+    show_if: 
+      is: "postgres11"
+      variable: db_family
+    contents:
+    - type: select
+      label:  Database Version
+      variable: db_engine_version
+      settings:
+        default: "11.13"
+        options:
+        - label: "v11.1"
+          value: "11.1"
+        - label: "v11.2"
+          value: "11.2"
+        - label: "v11.3"
+          value: "11.3"
+        - label: "v11.4"
+          value: "11.4"
+        - label: "v11.5"
+          value: "11.5"
+        - label: "v11.6"
+          value: "11.6"
+        - label: "v11.7"
+          value: "11.7"
+        - label: "v11.8"
+          value: "11.8"
+        - label: "v11.9"
+          value: "11.9"
+        - label: "v11.10"
+          value: "11.10"
+        - label: "v11.11"
+          value: "11.11"
+        - label: "v11.12"
+          value: "11.12"
+        - label: "v11.13"
+          value: "11.13"
+  - name: pg-12-versions
+    show_if: 
+      is: "postgres12"
+      variable: db_family
+    contents:
+    - type: select
+      label:  Database Version
+      variable: db_engine_version
+      settings:
+        default: "12.8"
+        options:
+        - label: "v12.2"
+          value: "12.2"
+        - label: "v12.3"
+          value: "12.3"
+        - label: "v12.4"
+          value: "12.4"
+        - label: "v12.5"
+          value: "12.5"
+        - label: "v12.6"
+          value: "12.6"
+        - label: "v12.7"
+          value: "12.7"
+        - label: "v12.8"
+          value: "12.8"
+  - name: pg-13-versions
+    show_if: 
+      is: "postgres13"
+      variable: db_family
+    contents:
+    - type: select
+      label:  Database Version
+      variable: db_engine_version
+      settings:
+        default: "13.4"
+        options:
+        - label: "v13.1"
+          value: "13.1"
+        - label: "v13.2"
+          value: "13.2"
+        - label: "v13.3"
+          value: "13.3"
+        - label: "v13.4"
+          value: "13.4"
+  - name: additional-settings
+    contents:
+    - type: heading
+      label: Additional Settings
+    - type: checkbox
+      variable: db_deletion_protection
+      label: Enable deletion protection for the database.
+      settings:
+        default: false
+- name: storage
+  label: Storage
+  sections:
+  - name: storage
+    contents:
+    - type: heading
+      label: Storage Settings
+    - type: number-input
+      label: Gigabytes
+      variable: db_allocated_storage
+      placeholder: "ex: 10"
+      settings:
+        default: 10
+    - type: number-input
+      label: Gigabytes
+      variable: db_max_allocated_storage
+      placeholder: "ex: 20"
+      settings:
+        default: 20
+    - type: checkbox
+      variable: db_storage_encrypted
+      label: Enable storage encryption for the database. 
+      settings:
+        default: false`
+
+const ecrForm = `name: ECR
+hasSource: false
+includeHiddenFields: true
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: ECR Configuration
+    - type: string-input
+      label: ECR Name
+      required: true
+      placeholder: my-awesome-registry
+      variable: ecr_name
+`
+
+const eksForm = `name: EKS
+hasSource: false
+includeHiddenFields: true
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: EKS Configuration
+    - type: select
+      label: ⚙️ AWS Machine Type
+      variable: machine_type
+      settings:
+        default: t2.medium
+        options:
+        - label: t2.medium
+          value: t2.medium
+    - type: string-input
+      label: 👤 Issuer Email
+      required: true
+      placeholder: example@example.com
+      variable: issuer_email
+    - type: string-input
+      label: EKS Cluster Name
+      required: true
+      placeholder: my-cluster
+      variable: cluster_name
+`
+
+const gcrForm = `name: GCR
+hasSource: false
+includeHiddenFields: true
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: GCR Configuration
+    - type: select
+      label: 📍 GCP Region
+      variable: gcp_region
+      settings:
+        default: us-central1
+        options:
+        - label: asia-east1
+          value: asia-east1
+        - label: asia-east2
+          value: asia-east2
+        - label: asia-northeast1
+          value: asia-northeast1
+        - label: asia-northeast2
+          value: asia-northeast2
+        - label: asia-northeast3
+          value: asia-northeast3
+        - label: asia-south1
+          value: asia-south1
+        - label: asia-southeast1
+          value: asia-southeast1
+        - label: asia-southeast2
+          value: asia-southeast2
+        - label: australia-southeast1
+          value: australia-southeast1
+        - label: europe-north1
+          value: europe-north1
+        - label: europe-west1
+          value: europe-west1
+        - label: europe-west2
+          value: europe-west2
+        - label: europe-west3
+          value: europe-west3
+        - label: europe-west4
+          value: europe-west4
+        - label: europe-west6
+          value: europe-west6
+        - label: northamerica-northeast1
+          value: northamerica-northeast1
+        - label: southamerica-east1
+          value: southamerica-east1
+        - label: us-central1
+          value: us-central1
+        - label: us-east1
+          value: us-east1
+        - label: us-east4
+          value: us-east4
+        - label: us-east1
+          value: us-east1
+        - label: us-east1
+          value: us-east1
+        - label: us-west1
+          value: us-west1
+        - label: us-east1
+          value: us-west2
+        - label: us-west3
+          value: us-west3
+        - label: us-west4
+          value: us-west4
+`
+
+const gkeForm = `name: GKE
+hasSource: false
+includeHiddenFields: true
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: GKE Configuration
+    - type: select
+      label: 📍 GCP Region
+      variable: gcp_region
+      settings:
+        default: us-central1
+        options:
+        - label: asia-east1
+          value: asia-east1
+        - label: asia-east2
+          value: asia-east2
+        - label: asia-northeast1
+          value: asia-northeast1
+        - label: asia-northeast2
+          value: asia-northeast2
+        - label: asia-northeast3
+          value: asia-northeast3
+        - label: asia-south1
+          value: asia-south1
+        - label: asia-southeast1
+          value: asia-southeast1
+        - label: asia-southeast2
+          value: asia-southeast2
+        - label: australia-southeast1
+          value: australia-southeast1
+        - label: europe-north1
+          value: europe-north1
+        - label: europe-west1
+          value: europe-west1
+        - label: europe-west2
+          value: europe-west2
+        - label: europe-west3
+          value: europe-west3
+        - label: europe-west4
+          value: europe-west4
+        - label: europe-west6
+          value: europe-west6
+        - label: northamerica-northeast1
+          value: northamerica-northeast1
+        - label: southamerica-east1
+          value: southamerica-east1
+        - label: us-central1
+          value: us-central1
+        - label: us-east1
+          value: us-east1
+        - label: us-east4
+          value: us-east4
+        - label: us-east1
+          value: us-east1
+        - label: us-east1
+          value: us-east1
+        - label: us-west1
+          value: us-west1
+        - label: us-east1
+          value: us-west2
+        - label: us-west3
+          value: us-west3
+        - label: us-west4
+          value: us-west4
+    - type: string-input
+      label: 👤 Issuer Email
+      required: true
+      placeholder: example@example.com
+      variable: issuer_email
+    - type: string-input
+      label: GKE Cluster Name
+      required: true
+      placeholder: my-cluster
+      variable: cluster_name
+`
+
+const docrForm = `name: DOCR
+hasSource: false
+includeHiddenFields: true
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: DOCR Configuration
+    - type: select
+      label: DO Subscription Tier
+      variable: docr_subscription_tier
+      settings:
+        default: basic
+        options:
+        - label: Basic
+          value: basic
+        - label: Professional
+          value: professional
+    - type: string-input
+      label: DOCR Name
+      required: true
+      placeholder: my-awesome-registry
+      variable: docr_name
+`
+
+const doksForm = `name: DOKS
+hasSource: false
+includeHiddenFields: true
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: DOKS Configuration
+    - type: select
+      label: 📍 DO Region
+      variable: do_region
+      settings:
+        default: nyc1
+        options:
+        - label: Amsterdam 3
+          value: ams3
+        - label: Bangalore 1
+          value: blr1
+        - label: Frankfurt 1
+          value: fra1
+        - label: London 1
+          value: lon1
+        - label: New York 1
+          value: nyc1
+        - label: New York 3
+          value: nyc3
+        - label: San Francisco 2
+          value: sfo2
+        - label: San Francisco 3
+          value: sfo3
+        - label: Singapore 1
+          value: sgp1
+        - label: Toronto 1
+          value: tor1
+    - type: string-input
+      label: 👤 Issuer Email
+      required: true
+      placeholder: example@example.com
+      variable: issuer_email
+    - type: string-input
+      label: DOKS Cluster Name
+      required: true
+      placeholder: my-cluster
+      variable: cluster_name
+`

+ 29 - 1
api/server/handlers/infra/get.go

@@ -1,13 +1,17 @@
 package infra
 
 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/types"
 	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
 )
 
 type InfraGetHandler struct {
@@ -26,5 +30,29 @@ func NewInfraGetHandler(
 func (c *InfraGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
 
-	c.WriteResult(w, r, infra.ToInfraType())
+	res := infra.ToInfraType()
+
+	// look for the latest operation and attach it, if it exists
+	operation, err := c.Repo().Infra().GetLatestOperation(infra)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("latest operation not found")))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	op, err := operation.ToOperationType()
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res.LatestOperation = op
+
+	c.WriteResult(w, r, res)
 }

+ 0 - 51
api/server/handlers/infra/get_current.go

@@ -1,51 +0,0 @@
-package infra
-
-import (
-	"errors"
-	"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/ee/integrations/httpbackend"
-	"github.com/porter-dev/porter/internal/models"
-)
-
-type InfraGetCurrentHandler struct {
-	handlers.PorterHandlerWriter
-}
-
-func NewInfraGetCurrentHandler(
-	config *config.Config,
-	writer shared.ResultWriter,
-) *InfraGetCurrentHandler {
-	return &InfraGetCurrentHandler{
-		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
-	}
-}
-
-func (c *InfraGetCurrentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
-
-	// TODO: move client out of this call
-	client := httpbackend.NewClient(c.Config().ServerConf.ProvisionerBackendURL)
-
-	// get the unique infra name and query from the TF HTTP backend
-	current, err := client.GetCurrentState(infra.GetUniqueName())
-
-	if err != nil && errors.Is(err, httpbackend.ErrNotFound) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			err,
-			http.StatusNotFound,
-		))
-
-		return
-	} else if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.WriteResult(w, r, current)
-}

+ 0 - 51
api/server/handlers/infra/get_desired.go

@@ -1,51 +0,0 @@
-package infra
-
-import (
-	"errors"
-	"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/ee/integrations/httpbackend"
-	"github.com/porter-dev/porter/internal/models"
-)
-
-type InfraGetDesiredHandler struct {
-	handlers.PorterHandlerWriter
-}
-
-func NewInfraGetDesiredHandler(
-	config *config.Config,
-	writer shared.ResultWriter,
-) *InfraGetDesiredHandler {
-	return &InfraGetDesiredHandler{
-		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
-	}
-}
-
-func (c *InfraGetDesiredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
-
-	// TODO: move client out of this call
-	client := httpbackend.NewClient(c.Config().ServerConf.ProvisionerBackendURL)
-
-	// get the unique infra name and query from the TF HTTP backend
-	desired, err := client.GetDesiredState(infra.GetUniqueName())
-
-	if err != nil && errors.Is(err, httpbackend.ErrNotFound) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			err,
-			http.StatusNotFound,
-		))
-
-		return
-	} else if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.WriteResult(w, r, desired)
-}

+ 52 - 0
api/server/handlers/infra/get_operation.go

@@ -0,0 +1,52 @@
+package infra
+
+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"
+	"github.com/porter-dev/porter/internal/templater/parser"
+)
+
+type InfraGetOperationHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewInfraGetOperationHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *InfraGetOperationHandler {
+	return &InfraGetOperationHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *InfraGetOperationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+	operation, _ := r.Context().Value(types.OperationScope).(*models.Operation)
+
+	op, err := operation.ToOperationType()
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// TODO: get corresponding form rather than test form
+	formYAML, err := parser.FormYAMLFromBytes(&parser.ClientConfigDefault{
+		InfraOperation: operation,
+	}, getFormBytesFromKind(string(infra.Kind)), "declared", "infra")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	op.Form = formYAML
+
+	c.WriteResult(w, r, op)
+}

+ 43 - 0
api/server/handlers/infra/get_operation_logs.go

@@ -0,0 +1,43 @@
+package infra
+
+import (
+	"context"
+	"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 InfraGetOperationLogsHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewInfraGetOperationLogsHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *InfraGetOperationLogsHandler {
+	return &InfraGetOperationLogsHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *InfraGetOperationLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+	operation, _ := r.Context().Value(types.OperationScope).(*models.Operation)
+
+	workspaceID := models.GetWorkspaceID(infra, operation)
+
+	// call apply on the provisioner service
+	resp, err := c.Config().ProvisionerClient.GetLogs(context.Background(), workspaceID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, resp)
+}

+ 41 - 0
api/server/handlers/infra/get_state.go

@@ -0,0 +1,41 @@
+package infra
+
+import (
+	"context"
+	"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 InfraGetStateHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewInfraGetStateHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *InfraGetStateHandler {
+	return &InfraGetStateHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *InfraGetStateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+
+	// call apply on the provisioner service
+	resp, err := c.Config().ProvisionerClient.GetState(context.Background(), proj.ID, infra.ID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, resp)
+}

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

@@ -0,0 +1,83 @@
+package infra
+
+import (
+	"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/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/templater/parser"
+)
+
+type InfraGetTemplateHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewInfraGetTemplateHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *InfraGetTemplateHandler {
+	return &InfraGetTemplateHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *InfraGetTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	_, reqErr := requestutils.GetURLParamString(r, types.URLParamTemplateVersion)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	name, reqErr := requestutils.GetURLParamString(r, types.URLParamTemplateName)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+
+		return
+	}
+
+	nameLower := strings.ToLower(name)
+
+	formYAML, err := parser.FormYAMLFromBytes(&parser.ClientConfigDefault{}, getFormBytesFromKind(name), "declared", "infra")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := &types.InfraTemplate{
+		InfraTemplateMeta: templateMap[nameLower],
+		Form:              formYAML,
+	}
+
+	c.WriteResult(w, r, res)
+}
+
+func getFormBytesFromKind(kind string) []byte {
+	formBytes := []byte(testForm)
+
+	switch strings.ToLower(kind) {
+	case "ecr":
+		formBytes = []byte(ecrForm)
+	case "rds":
+		formBytes = []byte(rdsForm)
+	case "eks":
+		formBytes = []byte(eksForm)
+	case "gcr":
+		formBytes = []byte(gcrForm)
+	case "gke":
+		formBytes = []byte(gkeForm)
+	case "docr":
+		formBytes = []byte(docrForm)
+	case "doks":
+		formBytes = []byte(doksForm)
+	}
+
+	return formBytes
+}

+ 10 - 3
api/server/handlers/infra/list.go

@@ -12,22 +12,29 @@ import (
 )
 
 type InfraListHandler struct {
-	handlers.PorterHandlerWriter
+	handlers.PorterHandlerReadWriter
 }
 
 func NewInfraListHandler(
 	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *InfraListHandler {
 	return &InfraListHandler{
-		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
 	}
 }
 
 func (p *InfraListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	infras, err := p.Repo().Infra().ListInfrasByProjectID(proj.ID)
+	req := &types.ListInfraRequest{}
+
+	if ok := p.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	infras, err := p.Repo().Infra().ListInfrasByProjectID(proj.ID, req.Version)
 
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 44 - 0
api/server/handlers/infra/list_operations.go

@@ -0,0 +1,44 @@
+package infra
+
+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 InfraListOperationsHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewInfraListOperationsHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *InfraListOperationsHandler {
+	return &InfraListOperationsHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *InfraListOperationsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+
+	ops, err := c.Repo().Infra().ListOperations(infra.ID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := make([]*types.OperationMeta, 0)
+
+	for _, op := range ops {
+		res = append(res, op.ToOperationMetaType())
+	}
+
+	c.WriteResult(w, r, res)
+}

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

@@ -0,0 +1,100 @@
+package infra
+
+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/config"
+	"github.com/porter-dev/porter/api/types"
+)
+
+type InfraListTemplateHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewInfraListTemplateHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *InfraListTemplateHandler {
+	return &InfraListTemplateHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *InfraListTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	res := make([]types.InfraTemplateMeta, 0)
+
+	for _, val := range templateMap {
+		res = append(res, *val)
+	}
+
+	c.WriteResult(w, r, res)
+}
+
+var templateMap = map[string]*types.InfraTemplateMeta{
+	"test": {
+		Icon:               "",
+		Description:        "Create a test resource.",
+		Name:               "Test",
+		Version:            "v0.1.0",
+		Kind:               "test",
+		RequiredCredential: "do_integration_id",
+	},
+	"ecr": {
+		Icon:               "https://avatars2.githubusercontent.com/u/52505464?s=400&u=da920f994c67665c7ad6c606a5286557d4f8555f&v=4",
+		Description:        "Create an Elastic Container Registry.",
+		Name:               "ECR",
+		Version:            "v0.1.0",
+		Kind:               "ecr",
+		RequiredCredential: "aws_integration_id",
+	},
+	"rds": {
+		Icon:               "",
+		Description:        "Create a Relational Database Service instance.",
+		Name:               "RDS",
+		Version:            "v0.1.0",
+		Kind:               "rds",
+		RequiredCredential: "aws_integration_id",
+	},
+	"eks": {
+		Icon:               "https://img.stackshare.io/service/7991/amazon-eks.png",
+		Description:        "Create an Elastic Kubernetes Service cluster.",
+		Name:               "EKS",
+		Version:            "v0.1.0",
+		Kind:               "eks",
+		RequiredCredential: "aws_integration_id",
+	},
+	"gcr": {
+		Icon:               "https://carlossanchez.files.wordpress.com/2019/06/21046548.png?w=640",
+		Description:        "Create a Google Container Registry.",
+		Name:               "GCR",
+		Version:            "v0.1.0",
+		Kind:               "gcr",
+		RequiredCredential: "gcp_integration_id",
+	},
+	"gke": {
+		Icon:               "https://sysdig.com/wp-content/uploads/2016/08/GKE_color.png",
+		Description:        "Create a Google Kubernetes Engine cluster.",
+		Name:               "GKE",
+		Version:            "v0.1.0",
+		Kind:               "gke",
+		RequiredCredential: "gcp_integration_id",
+	},
+	"docr": {
+		Icon:               "",
+		Description:        "Create a Digital Ocean Container Registry.",
+		Name:               "DOCR",
+		Version:            "v0.1.0",
+		Kind:               "docr",
+		RequiredCredential: "do_integration_id",
+	},
+	"doks": {
+		Icon:               "",
+		Description:        "Create a Digital Ocean Kubernetes Service cluster.",
+		Name:               "DOKS",
+		Version:            "v0.1.0",
+		Kind:               "doks",
+		RequiredCredential: "do_integration_id",
+	},
+}

+ 0 - 195
api/server/handlers/infra/retry.go

@@ -1,195 +0,0 @@
-package infra
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/provision"
-	"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/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/ecr"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/eks"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/do/docr"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/do/doks"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp/gcr"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp/gke"
-	"github.com/porter-dev/porter/internal/models"
-	"gorm.io/gorm"
-)
-
-type InfraRetryHandler struct {
-	handlers.PorterHandlerWriter
-}
-
-func NewInfraRetryHandler(config *config.Config, writer shared.ResultWriter) *InfraRetryHandler {
-	return &InfraRetryHandler{
-		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
-	}
-}
-
-func (c *InfraRetryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	infraModel, _ := r.Context().Value(types.InfraScope).(*models.Infra)
-
-	if infraModel.Status != types.StatusError {
-		c.HandleAPIError(w, r, apierrors.NewErrForbidden(
-			fmt.Errorf("only errored infras maybe retried")))
-		return
-	}
-
-	opts, err := c.getProvisioningOpts(infraModel)
-	if err != nil {
-		c.HandleAPIError(w, r, err)
-		return
-	}
-
-	opts.OperationKind = provisioner.Apply
-
-	provisionerErr := c.Config().ProvisionerAgent.Provision(opts)
-	if provisionerErr != nil {
-		infraModel.Status = types.StatusError
-		c.Repo().Infra().UpdateInfra(infraModel)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(provisionerErr))
-		return
-	}
-
-	infraModel.Status = types.StatusCreating
-	infraModel, _ = c.Repo().Infra().UpdateInfra(infraModel)
-
-	c.WriteResult(w, r, infraModel.ToInfraType())
-}
-
-func (c *InfraRetryHandler) getProvisioningOpts(infraModel *models.Infra) (*provisioner.ProvisionOpts, apierrors.RequestError) {
-	var vaultToken string
-	var opts *provisioner.ProvisionOpts
-
-	infra := infraModel.ToInfraType()
-
-	switch infra.Kind {
-	// ==================== Infrastructure Google Cloud ======================
-	case types.InfraGKE, types.InfraGCR:
-		integration, err := c.Repo().GCPIntegration().ReadGCPIntegration(infra.ProjectID, infra.GCPIntegrationID)
-		if err != nil {
-			return nil, c._qualifyGormError(err)
-		}
-
-		opts, err = c.getOptions(infraModel)
-		if err != nil {
-			return nil, apierrors.NewErrInternal(err)
-		}
-
-		if c.Config().CredentialBackend != nil {
-			vaultToken, err = c.Config().CredentialBackend.CreateGCPToken(integration)
-			if err != nil {
-				return nil, apierrors.NewErrInternal(err)
-			}
-		}
-
-		if infra.Kind == types.InfraGKE {
-			opts.GKE = &gke.Conf{
-				GCPProjectID: integration.GCPProjectID,
-				GCPRegion:    integration.GCPRegion,
-				ClusterName:  infra.LastApplied["gke_name"],
-			}
-		} else {
-			opts.GCR = &gcr.Conf{
-				GCPProjectID: integration.GCPProjectID,
-			}
-		}
-
-	// ========================== Infrastructure AWS ============================
-	case types.InfraEKS, types.InfraECR:
-		integration, err := c.Repo().AWSIntegration().ReadAWSIntegration(infra.ProjectID, infra.AWSIntegrationID)
-		if err != nil {
-			return nil, c._qualifyGormError(err)
-		}
-
-		opts, err = c.getOptions(infraModel)
-		if err != nil {
-			return nil, apierrors.NewErrInternal(err)
-		}
-
-		if c.Config().CredentialBackend != nil {
-			vaultToken, err = c.Config().CredentialBackend.CreateAWSToken(integration)
-			if err != nil {
-				return nil, apierrors.NewErrInternal(err)
-			}
-		}
-
-		if infra.Kind == types.InfraEKS {
-			opts.EKS = &eks.Conf{
-				AWSRegion:   integration.AWSRegion,
-				ClusterName: infra.LastApplied["eks_name"],
-				MachineType: infra.LastApplied["machine_type"],
-				IssuerEmail: infra.LastApplied["issuer_email"],
-			}
-		} else {
-			opts.ECR = &ecr.Conf{
-				AWSRegion: integration.AWSRegion,
-				ECRName:   infra.LastApplied["ecr_name"],
-			}
-		}
-
-	// ========================== Infrastructure Digital Ocean ============================
-	case types.InfraDOKS, types.InfraDOCR:
-		integration, err := c.Repo().OAuthIntegration().ReadOAuthIntegration(infra.ProjectID, infra.DOIntegrationID)
-		if err != nil {
-			return nil, c._qualifyGormError(err)
-		}
-
-		opts, err = c.getOptions(infraModel)
-		if err != nil {
-			return nil, apierrors.NewErrInternal(err)
-		}
-
-		if c.Config().CredentialBackend != nil {
-			vaultToken, err = c.Config().CredentialBackend.CreateOAuthToken(integration)
-			if err != nil {
-				return nil, apierrors.NewErrInternal(err)
-			}
-		}
-
-		if infra.Kind == types.InfraDOKS {
-			opts.DOKS = &doks.Conf{
-				DORegion:        infra.LastApplied["do_region"],
-				DOKSClusterName: infra.LastApplied["doks_name"],
-				IssuerEmail:     infra.LastApplied["issuer_email"],
-			}
-		} else {
-			opts.DOCR = &docr.Conf{
-				DOCRName:             infra.LastApplied["docr_name"],
-				DOCRSubscriptionTier: infra.LastApplied["docr_subscription_tier"],
-			}
-		}
-
-	default:
-		// infra == InfraTest
-		panic("not implemented!")
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-	opts.OperationKind = provisioner.Apply
-
-	return opts, nil
-}
-
-func (c *InfraRetryHandler) _qualifyGormError(err error) apierrors.RequestError {
-	if err == gorm.ErrRecordNotFound {
-		return apierrors.NewErrForbidden(err)
-	} else {
-		return apierrors.NewErrInternal(err)
-	}
-}
-
-func (c *InfraRetryHandler) getOptions(infraModel *models.Infra) (*provisioner.ProvisionOpts, error) {
-	// get provisioner options
-	opts, err := provision.GetSharedProvisionerOpts(c.Config(), infraModel)
-	if err != nil {
-		return nil, apierrors.NewErrInternal(err)
-	}
-
-	return opts, nil
-}

+ 127 - 0
api/server/handlers/infra/retry_create.go

@@ -0,0 +1,127 @@
+package infra
+
+import (
+	"context"
+	"encoding/json"
+	"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"
+	ptypes "github.com/porter-dev/porter/provisioner/types"
+	"gorm.io/gorm"
+)
+
+type InfraRetryCreateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewInfraRetryCreateHandler(config *config.Config, decoderValidator shared.RequestDecoderValidator, writer shared.ResultWriter) *InfraRetryCreateHandler {
+	return &InfraRetryCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *InfraRetryCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+
+	req := &types.RetryInfraRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	var cluster *models.Cluster
+	var err error
+
+	if infra.ParentClusterID != 0 {
+		cluster, err = c.Repo().Cluster().ReadCluster(proj.ID, infra.ParentClusterID)
+
+		if err != nil {
+			if err == gorm.ErrRecordNotFound {
+				c.HandleAPIError(w, r, apierrors.NewErrForbidden(
+					fmt.Errorf("cluster with id %d not found in project %d", infra.ParentClusterID, proj.ID),
+				))
+			} else {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			}
+
+			return
+		}
+	}
+
+	// verify the credentials
+	err = checkInfraCredentials(c.Config(), proj, infra, req.InfraCredentials)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	lastOperation, err := c.Repo().Infra().GetLatestOperation(infra)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// if the last operation is in a "starting" state, block apply
+	if lastOperation.Status == "starting" {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("Operation currently in progress. Please try again when latest operation has completed."),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	// if the values are nil, get the last applied values and marshal them
+	if req.Values == nil || len(req.Values) == 0 {
+
+		rawValues := lastOperation.LastApplied
+
+		err = json.Unmarshal(rawValues, &req.Values)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	vals := req.Values
+
+	// if this is cluster-scoped and the kind is RDS, run the postrenderer
+	if infra.ParentClusterID != 0 && infra.Kind == "rds" {
+		var ok bool
+
+		pr := &InfraRDSPostrenderer{
+			config: c.Config(),
+		}
+
+		if vals, ok = pr.Run(w, r, &Opts{
+			Cluster: cluster,
+			Values:  vals,
+		}); !ok {
+			return
+		}
+	}
+
+	// call apply on the provisioner service
+	resp, err := c.Config().ProvisionerClient.Apply(context.Background(), proj.ID, infra.ID, &ptypes.ApplyBaseRequest{
+		Kind:          string(infra.Kind),
+		Values:        vals,
+		OperationKind: "retry_create",
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, resp)
+}

+ 73 - 0
api/server/handlers/infra/retry_delete.go

@@ -0,0 +1,73 @@
+package infra
+
+import (
+	"context"
+	"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"
+	ptypes "github.com/porter-dev/porter/provisioner/types"
+)
+
+type InfraRetryDeleteHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewInfraRetryDeleteHandler(config *config.Config, decoderValidator shared.RequestDecoderValidator, writer shared.ResultWriter) *InfraRetryDeleteHandler {
+	return &InfraRetryDeleteHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *InfraRetryDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+
+	req := &types.DeleteInfraRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	// verify the credentials
+	err := checkInfraCredentials(c.Config(), proj, infra, req.InfraCredentials)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	lastOperation, err := c.Repo().Infra().GetLatestOperation(infra)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// if the last operation is in a "starting" state, block apply
+	if lastOperation.Status == "starting" {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("Operation currently in progress. Please try again when latest operation has completed."),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	// call apply on the provisioner service
+	resp, err := c.Config().ProvisionerClient.Delete(context.Background(), proj.ID, infra.ID, &ptypes.DeleteBaseRequest{
+		OperationKind: "retry_delete",
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, resp)
+}

+ 87 - 12
api/server/handlers/infra/stream_logs.go

@@ -1,7 +1,12 @@
 package infra
 
 import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
 	"net/http"
+	"sync"
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -9,39 +14,109 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/websocket"
 	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/redis_stream"
+	"github.com/porter-dev/porter/provisioner/pb"
 )
 
-type InfraStreamLogsHandler struct {
+type InfraStreamLogHandler struct {
 	handlers.PorterHandlerWriter
 }
 
-func NewInfraStreamLogsHandler(
+func NewInfraStreamLogHandler(
 	config *config.Config,
 	writer shared.ResultWriter,
-) *InfraStreamLogsHandler {
-	return &InfraStreamLogsHandler{
+) *InfraStreamLogHandler {
+	return &InfraStreamLogHandler{
 		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
 	}
 }
 
-func (c *InfraStreamLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (c *InfraStreamLogHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	safeRW := r.Context().Value(types.RequestCtxWebsocketKey).(*websocket.WebsocketSafeReadWriter)
 	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+	operation, _ := r.Context().Value(types.OperationScope).(*models.Operation)
+	workspaceID := models.GetWorkspaceID(infra, operation)
 
-	client, err := adapter.NewRedisClient(c.Config().RedisConf)
+	ctx, cancel := c.Config().ProvisionerClient.NewGRPCContext(workspaceID)
+
+	defer cancel()
+
+	stream, err := c.Config().ProvisionerClient.GRPCClient.GetLog(ctx, &pb.Infra{
+		ProjectId: int64(infra.ProjectID),
+		Id:        int64(infra.ID),
+		Suffix:    infra.Suffix,
+	})
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	err = redis_stream.ResourceStream(client, infra.GetUniqueName(), safeRW)
+	errorchan := make(chan error)
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	go func() {
+		wg.Wait()
+		close(errorchan)
+	}()
+
+	go func() {
+		defer wg.Done()
+
+		for {
+			if _, _, err := safeRW.ReadMessage(); err != nil {
+				errorchan <- nil
+				fmt.Println("closing websocket goroutine")
+				return
+			}
+		}
+	}()
+
+	go func() {
+		defer wg.Done()
+
+		for {
+			tfLog, err := stream.Recv()
+
+			if err != nil {
+				if err == io.EOF || errors.Is(ctx.Err(), context.Canceled) {
+					errorchan <- nil
+				} else {
+					errorchan <- err
+				}
+
+				fmt.Println("closing grpc goroutine")
+
+				return
+			}
+
+			_, err = safeRW.Write([]byte(tfLog.Log))
+
+			if err != nil {
+				errorchan <- nil
+				fmt.Println("closing grpc goroutine")
+				return
+			}
+		}
+
+	}()
+
+	for err = range errorchan {
+		if err != nil {
+			c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
+		}
+
+		// close the grpc stream: do not check for error case since the stream could already be
+		// closed
+		stream.CloseSend()
+
+		// close the websocket stream: do not check for error case since the WS could already be
+		// closed
+		safeRW.Close()
+
+		// cancel the context set for the grpc stream to ensure that Recv is unblocked
+		cancel()
 	}
 }

+ 116 - 0
api/server/handlers/infra/stream_state.go

@@ -0,0 +1,116 @@
+package infra
+
+import (
+	"context"
+	"errors"
+	"io"
+	"net/http"
+	"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/config"
+	"github.com/porter-dev/porter/api/server/shared/websocket"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/provisioner/pb"
+)
+
+type InfraStreamStateHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewInfraStreamStateHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *InfraStreamStateHandler {
+	return &InfraStreamStateHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *InfraStreamStateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	safeRW := r.Context().Value(types.RequestCtxWebsocketKey).(*websocket.WebsocketSafeReadWriter)
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+	operation, _ := r.Context().Value(types.OperationScope).(*models.Operation)
+	workspaceID := models.GetWorkspaceID(infra, operation)
+
+	ctx, cancel := c.Config().ProvisionerClient.NewGRPCContext(workspaceID)
+
+	defer cancel()
+
+	stream, err := c.Config().ProvisionerClient.GRPCClient.GetStateUpdate(ctx, &pb.Infra{
+		ProjectId: int64(infra.ProjectID),
+		Id:        int64(infra.ID),
+		Suffix:    infra.Suffix,
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	errorchan := make(chan error)
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	go func() {
+		wg.Wait()
+		close(errorchan)
+	}()
+
+	go func() {
+		defer wg.Done()
+
+		for {
+			if _, _, err := safeRW.ReadMessage(); err != nil {
+				errorchan <- nil
+				return
+			}
+		}
+	}()
+
+	go func() {
+		defer wg.Done()
+
+		for {
+
+			stateUpdate, err := stream.Recv()
+
+			if err != nil {
+				if err == io.EOF || errors.Is(ctx.Err(), context.Canceled) {
+					errorchan <- nil
+				} else {
+					errorchan <- err
+				}
+
+				return
+			}
+
+			err = safeRW.WriteJSON(stateUpdate)
+
+			if err != nil {
+				errorchan <- err
+			}
+		}
+	}()
+
+	for err = range errorchan {
+		if err != nil {
+			c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
+		}
+
+		// close the grpc stream: do not check for error case since the stream could already be
+		// closed
+		stream.CloseSend()
+
+		// close the websocket stream: do not check for error case since the WS could already be
+		// closed
+		safeRW.Close()
+
+		// cancel the context set for the grpc stream to ensure that Recv is unblocked
+		cancel()
+	}
+}

+ 126 - 0
api/server/handlers/infra/update.go

@@ -0,0 +1,126 @@
+package infra
+
+import (
+	"context"
+	"encoding/json"
+	"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"
+	ptypes "github.com/porter-dev/porter/provisioner/types"
+	"gorm.io/gorm"
+)
+
+type InfraUpdateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewInfraUpdateHandler(config *config.Config, decoderValidator shared.RequestDecoderValidator, writer shared.ResultWriter) *InfraUpdateHandler {
+	return &InfraUpdateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *InfraUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
+
+	req := &types.RetryInfraRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	var cluster *models.Cluster
+	var err error
+
+	if infra.ParentClusterID != 0 {
+		cluster, err = c.Repo().Cluster().ReadCluster(proj.ID, infra.ParentClusterID)
+
+		if err != nil {
+			if err == gorm.ErrRecordNotFound {
+				c.HandleAPIError(w, r, apierrors.NewErrForbidden(
+					fmt.Errorf("cluster with id %d not found in project %d", infra.ParentClusterID, proj.ID),
+				))
+			} else {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			}
+
+			return
+		}
+	}
+
+	// verify the credentials
+	err = checkInfraCredentials(c.Config(), proj, infra, req.InfraCredentials)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	lastOperation, err := c.Repo().Infra().GetLatestOperation(infra)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// if the last operation is in a "starting" state, block apply
+	if lastOperation.Status == "starting" {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("Operation currently in progress. Please try again when latest operation has completed."),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	// if the values are nil, get the last applied values and marshal them
+	if req.Values == nil || len(req.Values) == 0 {
+		rawValues := lastOperation.LastApplied
+
+		err = json.Unmarshal(rawValues, &req.Values)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	vals := req.Values
+
+	// if this is cluster-scoped and the kind is RDS, run the postrenderer
+	if infra.ParentClusterID != 0 && infra.Kind == "rds" {
+		var ok bool
+
+		pr := &InfraRDSPostrenderer{
+			config: c.Config(),
+		}
+
+		if vals, ok = pr.Run(w, r, &Opts{
+			Cluster: cluster,
+			Values:  vals,
+		}); !ok {
+			return
+		}
+	}
+
+	// call apply on the provisioner service
+	resp, err := c.Config().ProvisionerClient.Apply(context.Background(), proj.ID, infra.ID, &ptypes.ApplyBaseRequest{
+		Kind:          string(infra.Kind),
+		Values:        vals,
+		OperationKind: "update",
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, resp)
+}

+ 22 - 5
api/server/handlers/namespace/clone_env_group.go

@@ -2,6 +2,7 @@ package namespace
 
 import (
 	"net/http"
+	"strings"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -46,21 +47,37 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	envGroup, err := envgroup.GetEnvGroup(agent, request.Name, request.Namespace, request.Version)
+	cm, _, err := agent.GetLatestVersionedConfigMap(request.Name, namespace)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
+	secret, _, err := agent.GetLatestVersionedSecret(request.Name, namespace)
+
 	if request.CloneName == "" {
 		request.CloneName = request.Name
 	}
 
+	vars := make(map[string]string)
+	secretVars := make(map[string]string)
+
+	for key, val := range cm.Data {
+		if !strings.Contains(val, "PORTERSECRET") {
+			vars[key] = val
+		}
+	}
+
+	for key, val := range secret.Data {
+		secretVars[key] = string(val)
+	}
+
 	configMap, err := envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
-		Name:      request.CloneName,
-		Namespace: namespace,
-		Variables: envGroup.Variables,
+		Name:            request.CloneName,
+		Namespace:       request.Namespace,
+		Variables:       vars,
+		SecretVariables: secretVars,
 	})
 
 	if err != nil {
@@ -68,7 +85,7 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	envGroup, err = envgroup.ToEnvGroup(configMap)
+	envGroup, err := envgroup.ToEnvGroup(configMap)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 9 - 9
api/server/handlers/namespace/get_env_group.go

@@ -1,9 +1,9 @@
 package namespace
 
 import (
-	"errors"
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -11,7 +11,6 @@ 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/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -51,13 +50,14 @@ func (c *GetEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	envGroup, err := envgroup.GetEnvGroup(agent, request.Name, namespace, request.Version)
 
-	if err != nil && errors.Is(err, kubernetes.IsNotFoundError) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("env group not found"),
-			http.StatusNotFound,
-		))
-		return
-	} else if err != nil {
+	if err != nil {
+		if strings.Contains(err.Error(), "not found") {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("env group not found"),
+				http.StatusNotFound),
+			)
+			return
+		}
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 56 - 0
api/server/handlers/namespace/stream_job_runs.go

@@ -0,0 +1,56 @@
+package namespace
+
+import (
+	"net/http"
+	"strings"
+
+	"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/websocket"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type StreamJobRunsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewStreamJobRunsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *StreamJobRunsHandler {
+	return &StreamJobRunsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *StreamJobRunsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	safeRW := r.Context().Value(types.RequestCtxWebsocketKey).(*websocket.WebsocketSafeReadWriter)
+	namespace := r.Context().Value(types.NamespaceScope).(string)
+
+	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
+	}
+
+	if strings.ToLower(namespace) == "all" {
+		namespace = ""
+	}
+
+	err = agent.StreamJobs(namespace, "", safeRW)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+}

+ 9 - 9
api/server/handlers/project/create_test.go

@@ -26,8 +26,8 @@ func TestCreateProjectSuccessful(t *testing.T) {
 
 	handler := project.NewProjectCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -66,7 +66,7 @@ func TestFailingDecoderValidator(t *testing.T) {
 	handler := project.NewProjectCreateHandler(
 		config,
 		apitest.NewFailingDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -90,8 +90,8 @@ func TestFailingCreateMethod(t *testing.T) {
 
 	handler := project.NewProjectCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -115,8 +115,8 @@ func TestFailingCreateRoleMethod(t *testing.T) {
 
 	handler := project.NewProjectCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -140,8 +140,8 @@ func TestFailingReadMethod(t *testing.T) {
 
 	handler := project.NewProjectCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)

+ 1 - 1
api/server/handlers/project/get_test.go

@@ -29,7 +29,7 @@ func TestGetProjectSuccessful(t *testing.T) {
 
 	handler := project.NewProjectGetHandler(
 		config,
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)

+ 2 - 2
api/server/handlers/project/list_test.go

@@ -37,7 +37,7 @@ func TestListProjectsSuccessful(t *testing.T) {
 
 	handler := project.NewProjectListHandler(
 		config,
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -65,7 +65,7 @@ func TestFailingListMethod(t *testing.T) {
 
 	handler := project.NewProjectListHandler(
 		config,
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)

+ 0 - 70
api/server/handlers/provision/helpers.go

@@ -1,70 +0,0 @@
-package provision
-
-import (
-	"fmt"
-	"time"
-
-	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/random"
-	"golang.org/x/crypto/bcrypt"
-)
-
-func CreateCEToken(conf *config.Config, infra *models.Infra) (*models.CredentialsExchangeToken, string, error) {
-	// convert the form to a project model
-	expiry := time.Now().Add(6 * time.Hour)
-
-	rawToken, err := random.StringWithCharset(32, "")
-
-	if err != nil {
-		return nil, "", err
-	}
-
-	hashedToken, err := bcrypt.GenerateFromPassword([]byte(rawToken), 8)
-
-	if err != nil {
-		return nil, "", err
-	}
-
-	ceToken := &models.CredentialsExchangeToken{
-		ProjectID:       infra.ProjectID,
-		Expiry:          &expiry,
-		Token:           hashedToken,
-		DOCredentialID:  infra.DOIntegrationID,
-		AWSCredentialID: infra.AWSIntegrationID,
-		GCPCredentialID: infra.GCPIntegrationID,
-	}
-
-	// handle write to the database
-	ceToken, err = conf.Repo.CredentialsExchangeToken().CreateCredentialsExchangeToken(ceToken)
-
-	if err != nil {
-		return nil, "", err
-	}
-
-	return ceToken, rawToken, nil
-}
-
-func GetSharedProvisionerOpts(conf *config.Config, infra *models.Infra) (*provisioner.ProvisionOpts, error) {
-	ceToken, rawToken, err := CreateCEToken(conf, infra)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return &provisioner.ProvisionOpts{
-		DryRun:              true,
-		Infra:               infra,
-		ProvImageTag:        conf.ServerConf.ProvisionerImageTag,
-		ProvJobNamespace:    conf.ServerConf.ProvisionerJobNamespace,
-		ProvImagePullSecret: conf.ServerConf.ProvisionerImagePullSecret,
-		TFHTTPBackendURL:    conf.ServerConf.ProvisionerBackendURL,
-		ProvisionerTest:     conf.ServerConf.ProvisionerTest,
-		CredentialExchange: &provisioner.ProvisionCredentialExchange{
-			CredExchangeEndpoint: fmt.Sprintf("%s/api/internal/credentials", conf.ServerConf.ProvisionerCredExchangeURL),
-			CredExchangeToken:    rawToken,
-			CredExchangeID:       ceToken.ID,
-		},
-	}, nil
-}

+ 0 - 137
api/server/handlers/provision/provision_docr.go

@@ -1,137 +0,0 @@
-package provision
-
-import (
-	"encoding/json"
-	"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/analytics"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/do/docr"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-type ProvisionDOCRHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-func NewProvisionDOCRHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *ProvisionDOCRHandler {
-	return &ProvisionDOCRHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (c *ProvisionDOCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// read the user and project from context
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-
-	request := &types.CreateDOCRInfraRequest{
-		ProjectID: proj.ID,
-	}
-
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		return
-	}
-
-	// get the DO integration, to check that integration exists and belongs to the project
-	doInt, err := c.Repo().OAuthIntegration().ReadOAuthIntegration(proj.ID, request.DOIntegrationID)
-
-	if err != nil {
-		if err == gorm.ErrRecordNotFound {
-			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
-		} else {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		}
-
-		return
-	}
-
-	suffix, err := repository.GenerateRandomBytes(6)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	lastApplied, err := json.Marshal(request)
-
-	// parse infra last applied into DOCR config
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	infra := &models.Infra{
-		Kind:            types.InfraDOCR,
-		ProjectID:       proj.ID,
-		Suffix:          suffix,
-		Status:          types.StatusCreating,
-		DOIntegrationID: request.DOIntegrationID,
-		CreatedByUserID: user.ID,
-		LastApplied:     lastApplied,
-	}
-
-	// handle write to the database
-	infra, err = c.Repo().Infra().CreateInfra(infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	opts, err := GetSharedProvisionerOpts(c.Config(), infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	vaultToken := ""
-
-	if c.Config().CredentialBackend != nil {
-		vaultToken, err = c.Config().CredentialBackend.CreateOAuthToken(doInt)
-
-		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-	opts.DOCR = &docr.Conf{
-		DOCRName:             request.DOCRName,
-		DOCRSubscriptionTier: request.DOCRSubscriptionTier,
-	}
-
-	opts.OperationKind = provisioner.Apply
-
-	err = c.Config().ProvisionerAgent.Provision(opts)
-
-	if err != nil {
-		infra.Status = types.StatusError
-		infra, _ = c.Repo().Infra().UpdateInfra(infra)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.Config().AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
-		&analytics.RegistryProvisioningStartTrackOpts{
-			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, proj.ID),
-			RegistryType:           types.InfraDOCR,
-			InfraID:                infra.ID,
-		},
-	))
-
-	c.WriteResult(w, r, infra.ToInfraType())
-}

+ 0 - 138
api/server/handlers/provision/provision_doks.go

@@ -1,138 +0,0 @@
-package provision
-
-import (
-	"encoding/json"
-	"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/analytics"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/do/doks"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-type ProvisionDOKSHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-func NewProvisionDOKSHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *ProvisionDOKSHandler {
-	return &ProvisionDOKSHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (c *ProvisionDOKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// read the user and project from context
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-
-	request := &types.CreateDOKSInfraRequest{
-		ProjectID: proj.ID,
-	}
-
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		return
-	}
-
-	// get the DO integration, to check that integration exists and belongs to the project
-	doInt, err := c.Repo().OAuthIntegration().ReadOAuthIntegration(proj.ID, request.DOIntegrationID)
-
-	if err != nil {
-		if err == gorm.ErrRecordNotFound {
-			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
-		} else {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		}
-
-		return
-	}
-
-	suffix, err := repository.GenerateRandomBytes(6)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	lastApplied, err := json.Marshal(request)
-
-	// parse infra last applied into DOKS config
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	infra := &models.Infra{
-		Kind:            types.InfraDOKS,
-		ProjectID:       proj.ID,
-		Suffix:          suffix,
-		Status:          types.StatusCreating,
-		DOIntegrationID: request.DOIntegrationID,
-		CreatedByUserID: user.ID,
-		LastApplied:     lastApplied,
-	}
-
-	// handle write to the database
-	infra, err = c.Repo().Infra().CreateInfra(infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	opts, err := GetSharedProvisionerOpts(c.Config(), infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	vaultToken := ""
-
-	if c.Config().CredentialBackend != nil {
-		vaultToken, err = c.Config().CredentialBackend.CreateOAuthToken(doInt)
-
-		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-	opts.DOKS = &doks.Conf{
-		DORegion:        request.DORegion,
-		DOKSClusterName: request.DOKSName,
-		IssuerEmail:     request.IssuerEmail,
-	}
-
-	opts.OperationKind = provisioner.Apply
-
-	err = c.Config().ProvisionerAgent.Provision(opts)
-
-	if err != nil {
-		infra.Status = types.StatusError
-		infra, _ = c.Repo().Infra().UpdateInfra(infra)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.Config().AnalyticsClient.Track(analytics.ClusterProvisioningStartTrack(
-		&analytics.ClusterProvisioningStartTrackOpts{
-			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, proj.ID),
-			ClusterType:            types.InfraDOKS,
-			InfraID:                infra.ID,
-		},
-	))
-
-	c.WriteResult(w, r, infra.ToInfraType())
-}

+ 0 - 136
api/server/handlers/provision/provision_ecr.go

@@ -1,136 +0,0 @@
-package provision
-
-import (
-	"encoding/json"
-	"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/analytics"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/ecr"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-type ProvisionECRHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-func NewProvisionECRHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *ProvisionECRHandler {
-	return &ProvisionECRHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (c *ProvisionECRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// read the user and project from context
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-
-	request := &types.CreateECRInfraRequest{
-		ProjectID: proj.ID,
-	}
-
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		return
-	}
-
-	// get the AWS integration, to check that integration exists and belongs to the project
-	awsInt, err := c.Repo().AWSIntegration().ReadAWSIntegration(proj.ID, request.AWSIntegrationID)
-
-	if err != nil {
-		if err == gorm.ErrRecordNotFound {
-			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
-		} else {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		}
-
-		return
-	}
-
-	suffix, err := repository.GenerateRandomBytes(6)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	lastApplied, err := json.Marshal(request)
-
-	// parse infra last applied into ECR config
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	infra := &models.Infra{
-		Kind:             types.InfraECR,
-		ProjectID:        proj.ID,
-		Suffix:           suffix,
-		Status:           types.StatusCreating,
-		AWSIntegrationID: request.AWSIntegrationID,
-		CreatedByUserID:  user.ID,
-		LastApplied:      lastApplied,
-	}
-
-	// handle write to the database
-	infra, err = c.Repo().Infra().CreateInfra(infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	opts, err := GetSharedProvisionerOpts(c.Config(), infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	vaultToken := ""
-
-	if c.Config().CredentialBackend != nil {
-		vaultToken, err = c.Config().CredentialBackend.CreateAWSToken(awsInt)
-
-		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-	opts.ECR = &ecr.Conf{
-		AWSRegion: awsInt.AWSRegion,
-		ECRName:   request.ECRName,
-	}
-	opts.OperationKind = provisioner.Apply
-
-	err = c.Config().ProvisionerAgent.Provision(opts)
-
-	if err != nil {
-		infra.Status = types.StatusError
-		infra, _ = c.Repo().Infra().UpdateInfra(infra)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.Config().AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
-		&analytics.RegistryProvisioningStartTrackOpts{
-			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, proj.ID),
-			RegistryType:           types.InfraECR,
-			InfraID:                infra.ID,
-		},
-	))
-
-	c.WriteResult(w, r, infra.ToInfraType())
-}

+ 0 - 138
api/server/handlers/provision/provision_eks.go

@@ -1,138 +0,0 @@
-package provision
-
-import (
-	"encoding/json"
-	"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/analytics"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/eks"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-type ProvisionEKSHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-func NewProvisionEKSHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *ProvisionEKSHandler {
-	return &ProvisionEKSHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (c *ProvisionEKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// read the user and project from context
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-
-	request := &types.CreateEKSInfraRequest{
-		ProjectID: proj.ID,
-	}
-
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		return
-	}
-
-	// get the AWS integration, to check that integration exists and belongs to the project
-	awsInt, err := c.Repo().AWSIntegration().ReadAWSIntegration(proj.ID, request.AWSIntegrationID)
-
-	if err != nil {
-		if err == gorm.ErrRecordNotFound {
-			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
-		} else {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		}
-
-		return
-	}
-
-	suffix, err := repository.GenerateRandomBytes(6)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	lastApplied, err := json.Marshal(request)
-
-	// parse infra last applied into EKS config
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	infra := &models.Infra{
-		Kind:             types.InfraEKS,
-		ProjectID:        proj.ID,
-		Suffix:           suffix,
-		Status:           types.StatusCreating,
-		AWSIntegrationID: request.AWSIntegrationID,
-		CreatedByUserID:  user.ID,
-		LastApplied:      lastApplied,
-	}
-
-	// handle write to the database
-	infra, err = c.Repo().Infra().CreateInfra(infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	opts, err := GetSharedProvisionerOpts(c.Config(), infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	vaultToken := ""
-
-	if c.Config().CredentialBackend != nil {
-		vaultToken, err = c.Config().CredentialBackend.CreateAWSToken(awsInt)
-
-		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-	opts.EKS = &eks.Conf{
-		AWSRegion:   awsInt.AWSRegion,
-		ClusterName: request.EKSName,
-		MachineType: request.MachineType,
-		IssuerEmail: request.IssuerEmail,
-	}
-	opts.OperationKind = provisioner.Apply
-
-	err = c.Config().ProvisionerAgent.Provision(opts)
-
-	if err != nil {
-		infra.Status = types.StatusError
-		infra, _ = c.Repo().Infra().UpdateInfra(infra)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.Config().AnalyticsClient.Track(analytics.ClusterProvisioningStartTrack(
-		&analytics.ClusterProvisioningStartTrackOpts{
-			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, proj.ID),
-			ClusterType:            types.InfraEKS,
-			InfraID:                infra.ID,
-		},
-	))
-
-	c.WriteResult(w, r, infra.ToInfraType())
-}

+ 0 - 135
api/server/handlers/provision/provision_gcr.go

@@ -1,135 +0,0 @@
-package provision
-
-import (
-	"encoding/json"
-	"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/analytics"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp/gcr"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-type ProvisionGCRHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-func NewProvisionGCRHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *ProvisionGCRHandler {
-	return &ProvisionGCRHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (c *ProvisionGCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// read the user and project from context
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-
-	request := &types.CreateGCRInfraRequest{
-		ProjectID: proj.ID,
-	}
-
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		return
-	}
-
-	// get the GCP integration, to check that integration exists and belongs to the project
-	gcpInt, err := c.Repo().GCPIntegration().ReadGCPIntegration(proj.ID, request.GCPIntegrationID)
-
-	if err != nil {
-		if err == gorm.ErrRecordNotFound {
-			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
-		} else {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		}
-
-		return
-	}
-
-	suffix, err := repository.GenerateRandomBytes(6)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	lastApplied, err := json.Marshal(request)
-
-	// parse infra last applied into GCR config
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	infra := &models.Infra{
-		Kind:             types.InfraGCR,
-		ProjectID:        proj.ID,
-		Suffix:           suffix,
-		Status:           types.StatusCreating,
-		GCPIntegrationID: request.GCPIntegrationID,
-		CreatedByUserID:  user.ID,
-		LastApplied:      lastApplied,
-	}
-
-	// handle write to the database
-	infra, err = c.Repo().Infra().CreateInfra(infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	opts, err := GetSharedProvisionerOpts(c.Config(), infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	vaultToken := ""
-
-	if c.Config().CredentialBackend != nil {
-		vaultToken, err = c.Config().CredentialBackend.CreateGCPToken(gcpInt)
-
-		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
-		}
-	}
-
-	opts.GCR = &gcr.Conf{
-		GCPProjectID: gcpInt.GCPProjectID,
-	}
-	opts.CredentialExchange.VaultToken = vaultToken
-	opts.OperationKind = provisioner.Apply
-
-	err = c.Config().ProvisionerAgent.Provision(opts)
-
-	if err != nil {
-		infra.Status = types.StatusError
-		infra, _ = c.Repo().Infra().UpdateInfra(infra)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.Config().AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
-		&analytics.RegistryProvisioningStartTrackOpts{
-			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, proj.ID),
-			RegistryType:           types.InfraGCR,
-			InfraID:                infra.ID,
-		},
-	))
-
-	c.WriteResult(w, r, infra.ToInfraType())
-}

+ 0 - 139
api/server/handlers/provision/provision_gke.go

@@ -1,139 +0,0 @@
-package provision
-
-import (
-	"encoding/json"
-	"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/analytics"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp/gke"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-type ProvisionGKEHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-func NewProvisionGKEHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *ProvisionGKEHandler {
-	return &ProvisionGKEHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (c *ProvisionGKEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// read the user and project from context
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-
-	request := &types.CreateGKEInfraRequest{
-		ProjectID: proj.ID,
-	}
-
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		return
-	}
-
-	// get the GCP integration, to check that integration exists and belongs to the project
-	gcpInt, err := c.Repo().GCPIntegration().ReadGCPIntegration(proj.ID, request.GCPIntegrationID)
-
-	if err != nil {
-		if err == gorm.ErrRecordNotFound {
-			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
-		} else {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		}
-
-		return
-	}
-
-	suffix, err := repository.GenerateRandomBytes(6)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	lastApplied, err := json.Marshal(request)
-
-	// parse infra last applied into GKE config
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	infra := &models.Infra{
-		Kind:             types.InfraGKE,
-		ProjectID:        proj.ID,
-		Suffix:           suffix,
-		Status:           types.StatusCreating,
-		GCPIntegrationID: request.GCPIntegrationID,
-		CreatedByUserID:  user.ID,
-		LastApplied:      lastApplied,
-	}
-
-	// handle write to the database
-	infra, err = c.Repo().Infra().CreateInfra(infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	opts, err := GetSharedProvisionerOpts(c.Config(), infra)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	vaultToken := ""
-
-	if c.Config().CredentialBackend != nil {
-		vaultToken, err = c.Config().CredentialBackend.CreateGCPToken(gcpInt)
-
-		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
-		}
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-	opts.GKE = &gke.Conf{
-		GCPProjectID: gcpInt.GCPProjectID,
-		GCPRegion:    request.GCPRegion,
-		ClusterName:  request.GKEName,
-		IssuerEmail:  request.IssuerEmail,
-	}
-
-	opts.OperationKind = provisioner.Apply
-
-	err = c.Config().ProvisionerAgent.Provision(opts)
-
-	if err != nil {
-		infra.Status = types.StatusError
-		infra, _ = c.Repo().Infra().UpdateInfra(infra)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.Config().AnalyticsClient.Track(analytics.ClusterProvisioningStartTrack(
-		&analytics.ClusterProvisioningStartTrackOpts{
-			ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(user.ID, proj.ID),
-			ClusterType:            types.InfraGKE,
-			InfraID:                infra.ID,
-		},
-	))
-
-	c.WriteResult(w, r, infra.ToInfraType())
-}

+ 0 - 336
api/server/handlers/provision/provision_rds.go

@@ -1,336 +0,0 @@
-package provision
-
-import (
-	"encoding/json"
-	"errors"
-	"fmt"
-	"net/http"
-	"strconv"
-
-	"github.com/mitchellh/mapstructure"
-
-	"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/types"
-	"github.com/porter-dev/porter/ee/integrations/httpbackend"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/rds"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-type ProvisionRDSHandler struct {
-	handlers.PorterHandlerReadWriter
-	authz.KubernetesAgentGetter
-}
-
-func NewProvisionRDSHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *ProvisionRDSHandler {
-	return &ProvisionRDSHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (c *ProvisionRDSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-	namespace := r.Context().Value(types.NamespaceScope).(string)
-	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-
-	request := &types.CreateRDSInfraRequest{}
-
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		return
-	}
-
-	// validate db version and family
-	if v, ok := types.DBVersionMapping[types.Family(request.DBFamily)]; !ok {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			errors.New("DB family does not exist"), http.StatusBadRequest))
-
-		return
-	} else {
-		if !v.VersionExists(types.EngineVersion(request.DBEngineVersion)) {
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				errors.New("DB version not available for the given family"), http.StatusBadRequest))
-
-			return
-		}
-	}
-
-	dbVersion := types.EngineVersion(request.DBEngineVersion)
-
-	clusterInfra, err := c.Repo().Infra().ReadInfra(proj.ID, cluster.InfraID)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("empty cluster infra, projectID: %d, infraID: %d", proj.ID, cluster.InfraID),
-			http.StatusNotFound,
-		))
-
-		return
-	}
-
-	// get the tfstate from the HTTP backend using the infra ID
-
-	client := httpbackend.NewClient(c.Config().ServerConf.ProvisionerBackendURL)
-
-	// get the unique infra name and query from the TF HTTP backend
-	currentState, err := client.GetCurrentState(clusterInfra.GetUniqueName())
-	if err != nil && errors.Is(err, httpbackend.ErrNotFound) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			err,
-			http.StatusNotFound,
-		))
-
-		return
-	} else if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	var vpc, region string
-	var subnets []string
-
-	var opts *provisioner.ProvisionOpts
-	vaultToken := ""
-
-	vpc, subnets, err = c.ExtractVPCFromEKSTFState(currentState, "aws_eks_cluster.cluster")
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			err,
-			http.StatusInternalServerError,
-		))
-
-		return
-	}
-
-	suffix, err := repository.GenerateRandomBytes(6)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	dbInfra := &models.Infra{
-		ProjectID:       proj.ID,
-		Status:          types.StatusCreating,
-		Suffix:          suffix,
-		CreatedByUserID: user.ID,
-	}
-
-	switch clusterInfra.Kind {
-	case types.InfraGKE:
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			errors.New("unsupported cluster kind"),
-			http.StatusBadRequest,
-		))
-
-		return
-	case types.InfraEKS:
-		dbInfra.Kind = types.InfraRDS
-		dbInfra.AWSIntegrationID = clusterInfra.AWSIntegrationID
-
-		integration, err := c.Repo().AWSIntegration().ReadAWSIntegration(clusterInfra.ProjectID, clusterInfra.AWSIntegrationID)
-		if err != nil {
-			if err == gorm.ErrRecordNotFound {
-				c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
-			} else {
-				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			}
-
-			return
-		}
-
-		region = integration.AWSRegion
-
-		if c.Config().CredentialBackend != nil {
-			vaultToken, err = c.Config().CredentialBackend.CreateAWSToken(integration)
-			if err != nil {
-				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			}
-		}
-
-		vpc, subnets, err = c.ExtractVPCFromEKSTFState(currentState, "aws_eks_cluster.cluster")
-		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				err,
-				http.StatusInternalServerError,
-			))
-
-			return
-		}
-
-	case types.InfraDOKS:
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			errors.New("unsupported cluster kind"),
-			http.StatusBadRequest,
-		))
-
-		return
-	}
-
-	if len(subnets) != 3 {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			errors.New("Length of subnets is not 3: not a valid VPC"),
-			http.StatusNotImplemented,
-		))
-
-		return
-	}
-
-	lastAppliedData := &types.RDSInfraLastApplied{
-		CreateRDSInfraRequest: request,
-		ClusterID:             cluster.ID,
-		Namespace:             namespace,
-		AWSRegion:             region,
-		DBMajorEngineVersion:  dbVersion.MajorVersion(),
-		DBStorageEncrypted:    strconv.FormatBool(request.DBEncryption),
-		DeletionProtection:    strconv.FormatBool(true),
-		VPCID:                 vpc,
-		Subnet1:               subnets[0],
-		Subnet2:               subnets[1],
-		Subnet3:               subnets[2],
-	}
-
-	lastApplied, err := json.Marshal(lastAppliedData)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	dbInfra.LastApplied = lastApplied
-
-	// handle write to the database
-	infra, err := c.Repo().Infra().CreateInfra(dbInfra)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	opts, err = GetSharedProvisionerOpts(c.Config(), infra)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	opts.CredentialExchange.VaultToken = vaultToken
-
-	opts.RDS = &rds.Conf{
-		AWSRegion:             region,
-		DBName:                request.DBName,
-		MachineType:           request.MachineType,
-		DBEngineVersion:       request.DBEngineVersion,
-		DBFamily:              request.DBFamily,
-		DBMajorEngineVersion:  dbVersion.MajorVersion(),
-		DBAllocatedStorage:    request.DBStorage,
-		DBMaxAllocatedStorage: request.DBMaxStorage,
-		DBStorageEncrypted:    strconv.FormatBool(request.DBEncryption),
-		Username:              request.Username,
-		Password:              request.Password,
-		VPCID:                 vpc,
-		DeletionProtection:    strconv.FormatBool(true),
-		Subnet1:               subnets[0],
-		Subnet2:               subnets[1],
-		Subnet3:               subnets[2],
-	}
-
-	opts.OperationKind = provisioner.Apply
-
-	err = c.Config().ProvisionerAgent.Provision(opts)
-	if err != nil {
-		infra.Status = types.StatusError
-		infra, _ = c.Repo().Infra().UpdateInfra(infra)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.WriteResult(w, r, infra.ToInfraType())
-}
-
-func (c *ProvisionRDSHandler) ExtractVPCFromEKSTFState(tfState *httpbackend.TFState, resourceIdentifier string) (string, []string, error) {
-	for _, resource := range tfState.Resources {
-		if resourceIdentifier == resource.Type+"."+resource.Name {
-			for _, instance := range resource.Instances {
-				vpcConfig, ok := instance.Attributes["vpc_config"]
-				if !ok {
-					return "", []string{}, errors.New("name not found for the requested resource name-type")
-				}
-
-				awsVPCConfigIface, ok := vpcConfig.([]interface{})
-				if !ok {
-					fmt.Printf("%#v\n", vpcConfig)
-					return "", []string{}, errors.New("cannot cast returned value to vpc config")
-				}
-
-				if len(awsVPCConfigIface) == 0 {
-					return "", []string{}, errors.New("empty vpc config")
-				}
-
-				awsVPCConfigMap, ok := awsVPCConfigIface[0].(map[string]interface{})
-				if !ok {
-					return "", []string{}, errors.New("cannot cast returned value to vpc config map")
-				}
-
-				var awsVPCConfig httpbackend.AWSVPCConfig
-
-				err := mapstructure.Decode(awsVPCConfigMap, &awsVPCConfig)
-				if err != nil {
-					return "", []string{}, errors.New("cannot cast returned value to vpc config")
-				}
-
-				return awsVPCConfig.VPCID, awsVPCConfig.SubNetIDs, nil
-			}
-
-			return "", []string{}, errors.New("name not found for the requested resource name-type")
-			// return c._extractVPCFromResourceInstance(resource, "id")
-		}
-	}
-
-	return "", []string{}, errors.New("name not found for the requested resource name-type")
-}
-
-func (c *ProvisionRDSHandler) ExtractVPCFromGKETFState(tfState *httpbackend.TFState, resourceIdentifier string) (string, error) {
-	for _, resource := range tfState.Resources {
-		// fmt.Printf("%s.%s\n", resource.Type, resource.Name)
-
-		if resourceIdentifier == resource.Type+"."+resource.Name {
-			return c._extractVPCFromResourceInstance(resource, "name")
-		}
-	}
-
-	return "", errors.New("name not found for the requested resource name-type")
-}
-
-func (c *ProvisionRDSHandler) _extractVPCFromResourceInstance(resource httpbackend.TFStateResource, attributeName string) (string, error) {
-	for _, instance := range resource.Instances {
-		vpc, ok := instance.Attributes[attributeName]
-		if !ok {
-			return "", errors.New("name not found for the requested resource name-type")
-		}
-
-		vpcName, ok := vpc.(string)
-		if !ok {
-			return "", errors.New("cannot cast returned value to string")
-		}
-
-		return vpcName, nil
-	}
-
-	return "", errors.New("name not found for the requested resource name-type")
-}
-
-func (c *ProvisionRDSHandler) _qualifyGormError(err error) apierrors.RequestError {
-	if err == gorm.ErrRecordNotFound {
-		return apierrors.NewErrForbidden(err)
-	} else {
-		return apierrors.NewErrInternal(err)
-	}
-}

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

@@ -14,13 +14,13 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/encryption"
 	"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/models"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/registry"
-	"github.com/porter-dev/porter/internal/repository"
 	"gopkg.in/yaml.v2"
 	"helm.sh/helm/v3/pkg/release"
 )
@@ -164,7 +164,7 @@ func createReleaseFromHelmRelease(
 	projectID, clusterID uint,
 	helmRelease *release.Release,
 ) (*models.Release, error) {
-	token, err := repository.GenerateRandomBytes(16)
+	token, err := encryption.GenerateRandomBytes(16)
 
 	if err != nil {
 		return nil, err

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

@@ -15,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
+	"helm.sh/helm/v3/pkg/chart"
 )
 
 type CreateAddonHandler struct {
@@ -35,6 +36,7 @@ func NewCreateAddonHandler(
 
 func (c *CreateAddonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	operationID := oauth.CreateRandomState()
@@ -63,7 +65,12 @@ func (c *CreateAddonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		request.TemplateVersion = ""
 	}
 
-	chart, err := loader.LoadChartPublic(request.RepoURL, request.TemplateName, request.TemplateVersion)
+	chart, err := LoadChart(c.Config(), &LoadAddonChartOpts{
+		ProjectID:       proj.ID,
+		RepoURL:         request.RepoURL,
+		TemplateName:    request.TemplateName,
+		TemplateVersion: request.TemplateVersion,
+	})
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -112,3 +119,45 @@ func (c *CreateAddonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		},
 	))
 }
+
+type LoadAddonChartOpts struct {
+	ProjectID                              uint
+	RepoURL, TemplateName, TemplateVersion string
+}
+
+func LoadChart(config *config.Config, opts *LoadAddonChartOpts) (*chart.Chart, error) {
+	// if the chart repo url is one of the specified application/addon charts, just load public
+	if opts.RepoURL == config.ServerConf.DefaultAddonHelmRepoURL || opts.RepoURL == config.ServerConf.DefaultApplicationHelmRepoURL {
+		return loader.LoadChartPublic(opts.RepoURL, opts.TemplateName, opts.TemplateVersion)
+	} else {
+		// load the helm repos in the project
+		hrs, err := config.Repo.HelmRepo().ListHelmReposByProjectID(opts.ProjectID)
+
+		if err != nil {
+			return nil, err
+		}
+
+		for _, hr := range hrs {
+			if hr.RepoURL == opts.RepoURL {
+				if hr.BasicAuthIntegrationID != 0 {
+					// read the basic integration id
+					basic, err := config.Repo.BasicIntegration().ReadBasicIntegration(opts.ProjectID, hr.BasicAuthIntegrationID)
+
+					if err != nil {
+
+						return nil, err
+					}
+
+					return loader.LoadChart(&loader.BasicAuthClient{
+						Username: string(basic.Username),
+						Password: string(basic.Password),
+					}, hr.RepoURL, opts.TemplateName, opts.TemplateVersion)
+				} else {
+					return loader.LoadChartPublic(hr.RepoURL, opts.TemplateName, opts.TemplateVersion)
+				}
+			}
+		}
+	}
+
+	return nil, fmt.Errorf("chart repo not found")
+}

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

@@ -123,7 +123,7 @@ func (c *ReleaseGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if res.Form == nil {
 		// for now just case by name
 		if res.Release.Chart.Name() == "cert-manager" {
-			formYAML, err := parser.FormYAMLFromBytes(parserDef, []byte(certManagerForm), "")
+			formYAML, err := parser.FormYAMLFromBytes(parserDef, []byte(certManagerForm), "", "")
 
 			if err == nil {
 				res.Form = formYAML

+ 18 - 2
api/server/handlers/release/get_jobs.go

@@ -22,15 +22,22 @@ type GetJobsHandler struct {
 
 func NewGetJobsHandler(
 	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *GetJobsHandler {
 	return &GetJobsHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
 		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
 	}
 }
 
 func (c *GetJobsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	request := &types.GetJobsRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
 	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	agent, err := c.GetAgent(r, cluster, "")
@@ -40,7 +47,16 @@ func (c *GetJobsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	jobs, err := agent.ListJobsByLabel(helmRelease.Namespace, getJobLabels(helmRelease)...)
+	labels := getJobLabels(helmRelease)
+
+	if request.Revision != 0 {
+		labels = append(labels, kubernetes.Label{
+			Key: "helm.sh/revision",
+			Val: fmt.Sprintf("%d", request.Revision),
+		})
+	}
+
+	jobs, err := agent.ListJobsByLabel(helmRelease.Namespace, labels...)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 62 - 0
api/server/handlers/release/get_latest_job_run.go

@@ -0,0 +1,62 @@
+package release
+
+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/types"
+	"github.com/porter-dev/porter/internal/models"
+	"helm.sh/helm/v3/pkg/release"
+)
+
+type GetLatestJobRunHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetLatestJobRunHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetLatestJobRunHandler {
+	return &GetLatestJobRunHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *GetLatestJobRunHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
+	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
+	}
+
+	jobs, err := agent.ListJobsByLabel(helmRelease.Namespace, getJobLabels(helmRelease)...)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// get the most recent job
+	if len(jobs) > 0 {
+		mostRecentJob := jobs[0]
+
+		for _, job := range jobs {
+			createdAt := job.ObjectMeta.CreationTimestamp
+
+			if mostRecentJob.CreationTimestamp.Before(&createdAt) {
+				mostRecentJob = job
+			}
+		}
+
+		c.WriteResult(w, r, mostRecentJob)
+	}
+}

+ 11 - 6
api/server/handlers/release/ugprade.go

@@ -14,7 +14,6 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm"
-	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/integrations/slack"
 	"github.com/porter-dev/porter/internal/models"
 	"helm.sh/helm/v3/pkg/release"
@@ -94,11 +93,17 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 			}
 		}
 
-		chart, err := loader.LoadChartPublic(
-			chartRepoURL,
-			helmRelease.Chart.Metadata.Name,
-			request.ChartVersion,
-		)
+		chart, err := LoadChart(c.Config(), &LoadAddonChartOpts{
+			ProjectID:       cluster.ProjectID,
+			RepoURL:         chartRepoURL,
+			TemplateName:    helmRelease.Chart.Metadata.Name,
+			TemplateVersion: request.ChartVersion,
+		})
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(

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

@@ -66,7 +66,7 @@ func (t *TemplateGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	for _, file := range chart.Files {
 		if strings.Contains(file.Name, "form.yaml") {
-			formYAML, err := parser.FormYAMLFromBytes(parserDef, file.Data, "declared")
+			formYAML, err := parser.FormYAMLFromBytes(parserDef, file.Data, "declared", "")
 
 			if err != nil {
 				break

+ 1 - 1
api/server/handlers/template/list.go

@@ -47,7 +47,7 @@ func (t *TemplateListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	porterCharts := loader.RepoIndexToPorterChartList(repoIndex)
+	porterCharts := loader.RepoIndexToPorterChartList(repoIndex, repoURL)
 
 	t.WriteResult(w, r, porterCharts)
 }

+ 2 - 2
api/server/handlers/user/cli_login.go

@@ -12,8 +12,8 @@ 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/encryption"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
 )
 
 type CLILoginHandler struct {
@@ -64,7 +64,7 @@ func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// generate 64 characters long authorization code
-	code, err := repository.GenerateRandomBytes(32)
+	code, err := encryption.GenerateRandomBytes(32)
 
 	if err != nil {
 		err = fmt.Errorf("CLI random code generation failed: %s", err.Error())

+ 38 - 0
api/server/handlers/user/create.go

@@ -76,6 +76,13 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	err = addUserToDefaultProject(u.Config(), user)
+
+	if err != nil {
+		u.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	// save the user as authenticated in the session
 	redirect, err := authn.SaveUserAuthenticated(w, r, u.Config(), user)
 
@@ -113,3 +120,34 @@ func doesUserExist(userRepo repository.UserRepository, user *models.User) bool {
 
 	return user != nil && err == nil
 }
+
+// addUserToDefaultProject adds the created user to any default projects if required by
+// config variables.
+func addUserToDefaultProject(config *config.Config, user *models.User) error {
+	if config.ServerConf.InitInCluster {
+		// if this is the first user, add the user to the default project
+		if user.ID == 1 {
+			// read the default project
+			project, err := config.Repo.Project().ReadProject(1)
+
+			if err != nil {
+				return err
+			}
+
+			// create a new Role with the user as the admin
+			_, err = config.Repo.Project().CreateProjectRole(project, &models.Role{
+				Role: types.Role{
+					UserID:    user.ID,
+					ProjectID: project.ID,
+					Kind:      types.RoleAdmin,
+				},
+			})
+
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}

+ 12 - 12
api/server/handlers/user/create_test.go

@@ -27,8 +27,8 @@ func TestCreateUserSuccessful(t *testing.T) {
 
 	handler := user.NewUserCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -59,8 +59,8 @@ func TestCreateUserBadEmail(t *testing.T) {
 
 	handler := user.NewUserCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -84,8 +84,8 @@ func TestCreateUserMissingField(t *testing.T) {
 
 	handler := user.NewUserCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -113,8 +113,8 @@ func TestCreateUserSameEmail(t *testing.T) {
 
 	handler := user.NewUserCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -139,8 +139,8 @@ func TestFailingCreateUserMethod(t *testing.T) {
 
 	handler := user.NewUserCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -163,8 +163,8 @@ func TestFailingCreateSessionMethod(t *testing.T) {
 
 	handler := user.NewUserCreateHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)

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

@@ -18,7 +18,7 @@ func TestGetCurrentUserSuccessful(t *testing.T) {
 
 	handler := user.NewUserGetCurrentHandler(
 		config,
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)

+ 2 - 2
api/server/handlers/user/delete_test.go

@@ -26,7 +26,7 @@ func TestDeleteUserSuccessful(t *testing.T) {
 
 	handler := user.NewUserDeleteHandler(
 		config,
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -63,7 +63,7 @@ func TestFailingDeleteUserMethod(t *testing.T) {
 
 	handler := user.NewUserDeleteHandler(
 		config,
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)

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

@@ -89,7 +89,7 @@ func TestEmailVerifyFinalizeSuccessful(t *testing.T) {
 
 	handler := user.NewVerifyEmailFinalizeHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)

+ 6 - 0
api/server/handlers/user/github_callback.go

@@ -158,6 +158,12 @@ func upsertUserFromToken(config *config.Config, tok *oauth2.Token) (*models.User
 				return nil, err
 			}
 
+			err = addUserToDefaultProject(config, user)
+
+			if err != nil {
+				return nil, err
+			}
+
 			config.AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
 				UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
 				Email:               user.Email,

+ 6 - 0
api/server/handlers/user/google_callback.go

@@ -143,6 +143,12 @@ func upsertGoogleUserFromToken(config *config.Config, tok *oauth2.Token) (*model
 				return nil, err
 			}
 
+			err = addUserToDefaultProject(config, user)
+
+			if err != nil {
+				return nil, err
+			}
+
 			config.AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
 				UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
 				Email:               user.Email,

+ 12 - 12
api/server/handlers/user/login_test.go

@@ -28,8 +28,8 @@ func TestLoginUserSuccessful(t *testing.T) {
 
 	handler := user.NewUserLoginHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -61,8 +61,8 @@ func TestLoginUserIncorrectPassword(t *testing.T) {
 
 	handler := user.NewUserLoginHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -88,8 +88,8 @@ func TestLoginUserBadEmail(t *testing.T) {
 
 	handler := user.NewUserLoginHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -115,8 +115,8 @@ func TestLoginUserEmptyPassword(t *testing.T) {
 
 	handler := user.NewUserLoginHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -142,8 +142,8 @@ func TestLoginUserNotExist(t *testing.T) {
 
 	handler := user.NewUserLoginHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)
@@ -167,8 +167,8 @@ func TestLoginUserFailingReadUserByEmailMethod(t *testing.T) {
 
 	handler := user.NewUserLoginHandler(
 		config,
-		shared.NewDefaultRequestDecoderValidator(config),
-		shared.NewDefaultResultWriter(config),
+		shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
+		shared.NewDefaultResultWriter(config.Logger, config.Alerter),
 	)
 
 	handler.ServeHTTP(rr, req)

+ 58 - 0
api/server/router/helm_repo.go

@@ -80,5 +80,63 @@ func getHelmRepoRoutes(
 		Router:   r,
 	})
 
+	//  GET /api/projects/{project_id}/helmrepos/{helm_repo_id}/charts -> helmrepo.NewChartListHandler
+	hrListEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/charts",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.HelmRepoScope,
+			},
+		},
+	)
+
+	hrListHandler := helmrepo.NewChartListHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: hrListEndpoint,
+		Handler:  hrListHandler,
+		Router:   r,
+	})
+
+	//  GET /api/projects/{project_id}/helmrepos/{helm_repo_id}/charts/{name}/{version} -> helmrepo.NewChartGetHandler
+	chartGetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/charts/{name}/{version}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.HelmRepoScope,
+			},
+		},
+	)
+
+	chartGetHandler := helmrepo.NewChartGetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: chartGetEndpoint,
+		Handler:  chartGetHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 204 - 24
api/server/router/infra.go

@@ -1,7 +1,10 @@
 package router
 
 import (
+	"fmt"
+
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/database"
 	"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"
@@ -70,6 +73,7 @@ func getInfraRoutes(
 
 	listInfraHandler := infra.NewInfraListHandler(
 		config,
+		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
@@ -107,14 +111,14 @@ func getInfraRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/infras/{infra_id}/retry -> infra.NewInfraRetryHandler
-	retryProvisionEndpoint := factory.NewAPIEndpoint(
+	// POST /api/projects/{project_id}/infras/{infra_id}/retry_create -> infra.NewInfraRetryHandler
+	retryCreateEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbUpdate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/retry",
+				RelativePath: relPath + "/retry_create",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -124,82 +128,230 @@ func getInfraRoutes(
 		},
 	)
 
-	retryProvisionHandler := infra.NewInfraRetryHandler(
+	retryCreateHandler := infra.NewInfraRetryCreateHandler(
 		config,
+		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: retryProvisionEndpoint,
-		Handler:  retryProvisionHandler,
+		Endpoint: retryCreateEndpoint,
+		Handler:  retryCreateHandler,
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/infras/{infra_id}/logs -> infra.NewInfraStreamLogsHandler
-	streamLogsEndpoint := factory.NewAPIEndpoint(
+	// POST /api/projects/{project_id}/infras/{infra_id}/update -> infra.NewInfraUpdateHandler
+	updateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/update",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InfraScope,
+			},
+		},
+	)
+
+	updateHandler := infra.NewInfraUpdateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: updateEndpoint,
+		Handler:  updateHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/infras/{infra_id}/retry_delete -> infra.NewInfraRetryDeleteHandler
+	retryDeleteEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/retry_delete",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InfraScope,
+			},
+		},
+	)
+
+	retryDeleteHandler := infra.NewInfraRetryDeleteHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: retryDeleteEndpoint,
+		Handler:  retryDeleteHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/infras/{infra_id}/operations -> infra.NewInfraListOperationsHandler
+	listOperationsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/operations",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InfraScope,
+			},
+		},
+	)
+
+	listOperationsHandler := infra.NewInfraListOperationsHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listOperationsEndpoint,
+		Handler:  listOperationsHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/infras/{infra_id}/operations/{operation_id} -> infra.NewInfraGetOperationHandler
+	getOperationEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/operations/{%s}", relPath, types.URLParamOperationID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InfraScope,
+				types.OperationScope,
+			},
+		},
+	)
+
+	getOperationHandler := infra.NewInfraGetOperationHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getOperationEndpoint,
+		Handler:  getOperationHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/infras/{infra_id}/operations/{operation_id}/state -> infra.NewInfraStreamStateHandler
+	streamStateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/operations/{%s}/state", relPath, types.URLParamOperationID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InfraScope,
+				types.OperationScope,
+			},
+			IsWebsocket: true,
+		},
+	)
+
+	streamStateHandler := infra.NewInfraStreamStateHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: streamStateEndpoint,
+		Handler:  streamStateHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/infras/{infra_id}/operations/{operation_id}/log_stream -> infra.NewInfraStreamLogHandler
+	streamLogEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/logs",
+				RelativePath: fmt.Sprintf("%s/operations/{%s}/log_stream", relPath, types.URLParamOperationID),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
 				types.InfraScope,
+				types.OperationScope,
 			},
 			IsWebsocket: true,
 		},
 	)
 
-	streamLogsHandler := infra.NewInfraStreamLogsHandler(
+	streamLogHandler := infra.NewInfraStreamLogHandler(
 		config,
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: streamLogsEndpoint,
-		Handler:  streamLogsHandler,
+		Endpoint: streamLogEndpoint,
+		Handler:  streamLogHandler,
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/infras/{infra_id}/current -> infra.NewInfraGetHandler
-	getCurrentEndpoint := factory.NewAPIEndpoint(
+	// GET /api/projects/{project_id}/infras/{infra_id}/operations/{operation_id}/logs -> infra.NewInfraGetOperationLogsHandler
+	getOperationLogsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/current",
+				RelativePath: fmt.Sprintf("%s/operations/{%s}/logs", relPath, types.URLParamOperationID),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
 				types.InfraScope,
+				types.OperationScope,
 			},
 		},
 	)
 
-	getCurrentHandler := infra.NewInfraGetCurrentHandler(
+	getOperationLogsHandler := infra.NewInfraGetOperationLogsHandler(
 		config,
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: getCurrentEndpoint,
-		Handler:  getCurrentHandler,
+		Endpoint: getOperationLogsEndpoint,
+		Handler:  getOperationLogsHandler,
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/infras/{infra_id}/desired -> infra.NewInfraGetHandler
-	getDesiredEndpoint := factory.NewAPIEndpoint(
+	// GET /api/projects/{project_id}/infras/{infra_id}/state -> infra.NewInfraGetStateHandler
+	getStateEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/desired",
+				RelativePath: relPath + "/state",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -209,14 +361,14 @@ func getInfraRoutes(
 		},
 	)
 
-	getDesiredHandler := infra.NewInfraGetDesiredHandler(
+	getStateHandler := infra.NewInfraGetStateHandler(
 		config,
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: getDesiredEndpoint,
-		Handler:  getDesiredHandler,
+		Endpoint: getStateEndpoint,
+		Handler:  getStateHandler,
 		Router:   r,
 	})
 
@@ -249,5 +401,33 @@ func getInfraRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/infras/{infra_id}/database -> database.NewDatabaseUpdateStatusHandler
+	updateDBStatusEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/database",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InfraScope,
+			},
+		},
+	)
+
+	updateDBStatusHandler := database.NewDatabaseUpdateStatusHandler(
+		config,
+		factory.GetDecoderValidator(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: updateDBStatusEndpoint,
+		Handler:  updateDBStatusHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 1 - 1
api/server/router/middleware/panic.go

@@ -22,7 +22,7 @@ func (pmw *PanicMiddleware) Middleware(next http.Handler) http.Handler {
 			err := recover()
 
 			if err != nil {
-				apierrors.HandleAPIError(pmw.config, w, r, apierrors.NewErrInternal(fmt.Errorf("%v", err)), true)
+				apierrors.HandleAPIError(pmw.config.Logger, pmw.config.Alerter, w, r, apierrors.NewErrInternal(fmt.Errorf("%v", err)), true)
 			}
 		}()
 

+ 4 - 2
api/server/router/middleware/usage.go

@@ -36,7 +36,8 @@ func (b *UsageMiddleware) Middleware(next http.Handler) http.Handler {
 
 		if err != nil {
 			apierrors.HandleAPIError(
-				b.config,
+				b.config.Logger,
+				b.config.Alerter,
 				w, r,
 				apierrors.NewErrInternal(err),
 				true,
@@ -54,7 +55,8 @@ func (b *UsageMiddleware) Middleware(next http.Handler) http.Handler {
 			limit, curr := getMetricUsage(limit, currentUsage, b.metric)
 
 			apierrors.HandleAPIError(
-				b.config,
+				b.config.Logger,
+				b.config.Alerter,
 				w, r,
 				apierrors.NewErrPassThroughToClient(
 					fmt.Errorf(UsageErrFmt, b.metric, limit, curr),

+ 2 - 2
api/server/router/middleware/websocket.go

@@ -25,10 +25,10 @@ func (wm *WebsocketMiddleware) Middleware(next http.Handler) http.Handler {
 
 		if err != nil {
 			if errors.Is(err, websocket.UpgraderCheckOriginErr) {
-				apierrors.HandleAPIError(wm.config, w, r, apierrors.NewErrForbidden(err), true)
+				apierrors.HandleAPIError(wm.config.Logger, wm.config.Alerter, w, r, apierrors.NewErrForbidden(err), true)
 				return
 			} else {
-				apierrors.HandleAPIError(wm.config, w, r, apierrors.NewErrInternal(err), false)
+				apierrors.HandleAPIError(wm.config.Logger, wm.config.Alerter, w, r, apierrors.NewErrInternal(err), false)
 				return
 			}
 		}

+ 40 - 37
api/server/router/namespace.go

@@ -7,7 +7,6 @@ import (
 
 	"github.com/porter-dev/porter/api/server/handlers/job"
 	"github.com/porter-dev/porter/api/server/handlers/namespace"
-	"github.com/porter-dev/porter/api/server/handlers/provision"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
@@ -57,36 +56,6 @@ func getNamespaceRoutes(
 
 	routes := make([]*Route, 0)
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/provision/rds/ -> provision.NewProvisionRDSHandler
-	provisionRDSEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/provision/rds",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-				types.NamespaceScope,
-			},
-		},
-	)
-
-	provisionRDSHandler := provision.NewProvisionRDSHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: provisionRDSEndpoint,
-		Handler:  provisionRDSHandler,
-		Router:   r,
-	})
-
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/envgroups/list -> namespace.NewListEnvGroupsHandler
 	listEnvGroupsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
@@ -94,7 +63,7 @@ func getNamespaceRoutes(
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/envgroups/list",
+				RelativePath: relPath + "/envgroup/list",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -116,14 +85,14 @@ func getNamespaceRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/envgroups/clone -> namespace.NewCloneEnvGroupHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/envgroup/clone -> namespace.NewCloneEnvGroupHandler
 	cloneEnvGroupEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/envgroups/clone",
+				RelativePath: relPath + "/envgroup/clone",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -450,6 +419,40 @@ func getNamespaceRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/jobs/stream -> namespace.NewStreamJobRunsHandler
+	streamJobRunsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/jobs/stream",
+					relPath,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+			},
+			IsWebsocket: true,
+		},
+	)
+
+	streamJobRunsHandler := namespace.NewStreamJobRunsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: streamJobRunsEndpoint,
+		Handler:  streamJobRunsHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/pod/{name}/previous_logs
 	getPreviousLogsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
@@ -680,7 +683,7 @@ func getNamespaceRoutes(
 	})
 
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/ingresses/{name} ->
-	// release.NewGetJobsHandler
+	// namespace.NewGetIngressHandler
 	getIngressEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,

+ 214 - 74
api/server/router/project.go

@@ -1,12 +1,15 @@
 package router
 
 import (
+	"fmt"
+
 	"github.com/go-chi/chi"
 	"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/project"
-	"github.com/porter-dev/porter/api/server/handlers/provision"
 	"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"
@@ -632,72 +635,42 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
-	//  POST /api/projects/{project_id}/provision/ecr -> provision.NewProvisionECRHandler
-	provisionECREndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/provision/ecr",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-			},
-		},
-	)
-
-	provisionECRHandler := provision.NewProvisionECRHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: provisionECREndpoint,
-		Handler:  provisionECRHandler,
-		Router:   r,
-	})
-
-	//  POST /api/projects/{project_id}/provision/eks -> provision.NewProvisionEKSHandler
-	provisionEKSEndpoint := factory.NewAPIEndpoint(
+	// POST /api/projects/{project_id}/infras -> infra.NewInfraCreateHandler
+	createInfraEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/provision/eks",
+				RelativePath: relPath + "/infras",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
 			},
-			CheckUsage:  true,
-			UsageMetric: types.Clusters,
 		},
 	)
 
-	provisionEKSHandler := provision.NewProvisionEKSHandler(
+	createInfraHandler := infra.NewInfraCreateHandler(
 		config,
 		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: provisionEKSEndpoint,
-		Handler:  provisionEKSHandler,
+		Endpoint: createInfraEndpoint,
+		Handler:  createInfraHandler,
 		Router:   r,
 	})
 
-	//  POST /api/projects/{project_id}/provision/docr -> provision.NewProvisionDOCRHandler
-	provisionDOCREndpoint := factory.NewAPIEndpoint(
+	// GET /api/projects/{project_id}/infras/templates -> infra.NewInfraGetHandler
+	getTemplatesEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/provision/docr",
+				RelativePath: relPath + "/infras/templates",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -706,56 +679,226 @@ func getProjectRoutes(
 		},
 	)
 
-	provisionDOCRHandler := provision.NewProvisionDOCRHandler(
+	getTemplatesHandler := infra.NewInfraListTemplateHandler(
 		config,
-		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: provisionDOCREndpoint,
-		Handler:  provisionDOCRHandler,
+		Endpoint: getTemplatesEndpoint,
+		Handler:  getTemplatesHandler,
 		Router:   r,
 	})
 
-	//  POST /api/projects/{project_id}/provision/doks -> provision.NewProvisionDOKSHandler
-	provisionDOKSEndpoint := factory.NewAPIEndpoint(
+	// GET /api/projects/{project_id}/infras/templates -> infra.NewInfraGetHandler
+	getTemplateEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/provision/doks",
+				RelativePath: fmt.Sprintf("%s/infras/templates/{%s}/{%s}", relPath, types.URLParamTemplateName, types.URLParamTemplateVersion),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
 			},
-			CheckUsage:  true,
-			UsageMetric: types.Clusters,
 		},
 	)
 
-	provisionDOKSHandler := provision.NewProvisionDOKSHandler(
+	getTemplateHandler := infra.NewInfraGetTemplateHandler(
 		config,
-		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: provisionDOKSEndpoint,
-		Handler:  provisionDOKSHandler,
+		Endpoint: getTemplateEndpoint,
+		Handler:  getTemplateHandler,
 		Router:   r,
 	})
 
-	//  POST /api/projects/{project_id}/provision/gcr -> provision.NewProvisionGCRHandler
-	provisionGCREndpoint := factory.NewAPIEndpoint(
+	// //  POST /api/projects/{project_id}/provision/ecr -> provision.NewProvisionECRHandler
+	// provisionECREndpoint := factory.NewAPIEndpoint(
+	// 	&types.APIRequestMetadata{
+	// 		Verb:   types.APIVerbCreate,
+	// 		Method: types.HTTPVerbPost,
+	// 		Path: &types.Path{
+	// 			Parent:       basePath,
+	// 			RelativePath: relPath + "/provision/ecr",
+	// 		},
+	// 		Scopes: []types.PermissionScope{
+	// 			types.UserScope,
+	// 			types.ProjectScope,
+	// 		},
+	// 	},
+	// )
+
+	// provisionECRHandler := provision.NewProvisionECRHandler(
+	// 	config,
+	// 	factory.GetDecoderValidator(),
+	// 	factory.GetResultWriter(),
+	// )
+
+	// routes = append(routes, &Route{
+	// 	Endpoint: provisionECREndpoint,
+	// 	Handler:  provisionECRHandler,
+	// 	Router:   r,
+	// })
+
+	// //  POST /api/projects/{project_id}/provision/eks -> provision.NewProvisionEKSHandler
+	// provisionEKSEndpoint := factory.NewAPIEndpoint(
+	// 	&types.APIRequestMetadata{
+	// 		Verb:   types.APIVerbCreate,
+	// 		Method: types.HTTPVerbPost,
+	// 		Path: &types.Path{
+	// 			Parent:       basePath,
+	// 			RelativePath: relPath + "/provision/eks",
+	// 		},
+	// 		Scopes: []types.PermissionScope{
+	// 			types.UserScope,
+	// 			types.ProjectScope,
+	// 		},
+	// 		CheckUsage:  true,
+	// 		UsageMetric: types.Clusters,
+	// 	},
+	// )
+
+	// provisionEKSHandler := provision.NewProvisionEKSHandler(
+	// 	config,
+	// 	factory.GetDecoderValidator(),
+	// 	factory.GetResultWriter(),
+	// )
+
+	// routes = append(routes, &Route{
+	// 	Endpoint: provisionEKSEndpoint,
+	// 	Handler:  provisionEKSHandler,
+	// 	Router:   r,
+	// })
+
+	// //  POST /api/projects/{project_id}/provision/docr -> provision.NewProvisionDOCRHandler
+	// provisionDOCREndpoint := factory.NewAPIEndpoint(
+	// 	&types.APIRequestMetadata{
+	// 		Verb:   types.APIVerbCreate,
+	// 		Method: types.HTTPVerbPost,
+	// 		Path: &types.Path{
+	// 			Parent:       basePath,
+	// 			RelativePath: relPath + "/provision/docr",
+	// 		},
+	// 		Scopes: []types.PermissionScope{
+	// 			types.UserScope,
+	// 			types.ProjectScope,
+	// 		},
+	// 	},
+	// )
+
+	// provisionDOCRHandler := provision.NewProvisionDOCRHandler(
+	// 	config,
+	// 	factory.GetDecoderValidator(),
+	// 	factory.GetResultWriter(),
+	// )
+
+	// routes = append(routes, &Route{
+	// 	Endpoint: provisionDOCREndpoint,
+	// 	Handler:  provisionDOCRHandler,
+	// 	Router:   r,
+	// })
+
+	// //  POST /api/projects/{project_id}/provision/doks -> provision.NewProvisionDOKSHandler
+	// provisionDOKSEndpoint := factory.NewAPIEndpoint(
+	// 	&types.APIRequestMetadata{
+	// 		Verb:   types.APIVerbCreate,
+	// 		Method: types.HTTPVerbPost,
+	// 		Path: &types.Path{
+	// 			Parent:       basePath,
+	// 			RelativePath: relPath + "/provision/doks",
+	// 		},
+	// 		Scopes: []types.PermissionScope{
+	// 			types.UserScope,
+	// 			types.ProjectScope,
+	// 		},
+	// 		CheckUsage:  true,
+	// 		UsageMetric: types.Clusters,
+	// 	},
+	// )
+
+	// provisionDOKSHandler := provision.NewProvisionDOKSHandler(
+	// 	config,
+	// 	factory.GetDecoderValidator(),
+	// 	factory.GetResultWriter(),
+	// )
+
+	// routes = append(routes, &Route{
+	// 	Endpoint: provisionDOKSEndpoint,
+	// 	Handler:  provisionDOKSHandler,
+	// 	Router:   r,
+	// })
+
+	// //  POST /api/projects/{project_id}/provision/gcr -> provision.NewProvisionGCRHandler
+	// provisionGCREndpoint := factory.NewAPIEndpoint(
+	// 	&types.APIRequestMetadata{
+	// 		Verb:   types.APIVerbCreate,
+	// 		Method: types.HTTPVerbPost,
+	// 		Path: &types.Path{
+	// 			Parent:       basePath,
+	// 			RelativePath: relPath + "/provision/gcr",
+	// 		},
+	// 		Scopes: []types.PermissionScope{
+	// 			types.UserScope,
+	// 			types.ProjectScope,
+	// 		},
+	// 	},
+	// )
+
+	// provisionGCRHandler := provision.NewProvisionGCRHandler(
+	// 	config,
+	// 	factory.GetDecoderValidator(),
+	// 	factory.GetResultWriter(),
+	// )
+
+	// routes = append(routes, &Route{
+	// 	Endpoint: provisionGCREndpoint,
+	// 	Handler:  provisionGCRHandler,
+	// 	Router:   r,
+	// })
+
+	// //  POST /api/projects/{project_id}/provision/gke -> provision.NewProvisionGKEHandler
+	// provisionGKEEndpoint := factory.NewAPIEndpoint(
+	// 	&types.APIRequestMetadata{
+	// 		Verb:   types.APIVerbCreate,
+	// 		Method: types.HTTPVerbPost,
+	// 		Path: &types.Path{
+	// 			Parent:       basePath,
+	// 			RelativePath: relPath + "/provision/gke",
+	// 		},
+	// 		Scopes: []types.PermissionScope{
+	// 			types.UserScope,
+	// 			types.ProjectScope,
+	// 		},
+	// 		CheckUsage:  true,
+	// 		UsageMetric: types.Clusters,
+	// 	},
+	// )
+
+	// provisionGKEHandler := provision.NewProvisionGKEHandler(
+	// 	config,
+	// 	factory.GetDecoderValidator(),
+	// 	factory.GetResultWriter(),
+	// )
+
+	// routes = append(routes, &Route{
+	// 	Endpoint: provisionGKEEndpoint,
+	// 	Handler:  provisionGKEHandler,
+	// 	Router:   r,
+	// })
+
+	//  POST /api/projects/{project_id}/helmrepos -> helmrepo.NewHelmRepoCreateHandler
+	hrCreateEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/provision/gcr",
+				RelativePath: relPath + "/helmrepos",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -764,45 +907,42 @@ func getProjectRoutes(
 		},
 	)
 
-	provisionGCRHandler := provision.NewProvisionGCRHandler(
+	hrCreateHandler := helmrepo.NewHelmRepoCreateHandler(
 		config,
 		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: provisionGCREndpoint,
-		Handler:  provisionGCRHandler,
+		Endpoint: hrCreateEndpoint,
+		Handler:  hrCreateHandler,
 		Router:   r,
 	})
 
-	//  POST /api/projects/{project_id}/provision/gke -> provision.NewProvisionGKEHandler
-	provisionGKEEndpoint := factory.NewAPIEndpoint(
+	//  GET /api/projects/{project_id}/helmrepos -> helmrepo.NewHelmRepoListHandler
+	hrListEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/provision/gke",
+				RelativePath: relPath + "/helmrepos",
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
 			},
-			CheckUsage:  true,
-			UsageMetric: types.Clusters,
 		},
 	)
 
-	provisionGKEHandler := provision.NewProvisionGKEHandler(
+	hrListHandler := helmrepo.NewHelmRepoListHandler(
 		config,
-		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: provisionGKEEndpoint,
-		Handler:  provisionGKEHandler,
+		Endpoint: hrListEndpoint,
+		Handler:  hrListHandler,
 		Router:   r,
 	})
 

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

@@ -681,6 +681,7 @@ func getReleaseRoutes(
 
 	getJobsHandler := release.NewGetJobsHandler(
 		config,
+		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
@@ -690,6 +691,37 @@ func getReleaseRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/{name}/{version}/latest_job_run ->
+	// release.NewGetLatestJobRunHandler
+	getLatestJobRunEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/latest_job_run",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+				types.ReleaseScope,
+			},
+		},
+	)
+
+	getLatestJobRunHandler := release.NewGetLatestJobRunHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getLatestJobRunEndpoint,
+		Handler:  getLatestJobRunHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/{name}/{version}/jobs/status ->
 	// release.NewGetJobsHandler
 	getJobsStatusEndpoint := factory.NewAPIEndpoint(

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

@@ -183,6 +183,10 @@ func registerRoutes(config *config.Config, routes []*Route) {
 	// after authorization. Each subsequent http.Handler can lookup the infra in context.
 	infraFactory := authz.NewInfraScopedFactory(config)
 
+	// Create a new "operation-scoped" factory which will create a new operation-scoped request
+	// after authorization. Each subsequent http.Handler can lookup the operation in context.
+	operationFactory := authz.NewOperationScopedFactory(config)
+
 	// Create a new "release-scoped" factory which will create a new release-scoped request
 	// after authorization. Each subsequent http.Handler can lookup the release in context.
 	releaseFactory := authz.NewReleaseScopedFactory(config)
@@ -227,6 +231,8 @@ func registerRoutes(config *config.Config, routes []*Route) {
 				atomicGroup.Use(gitInstallationFactory.Middleware)
 			case types.InfraScope:
 				atomicGroup.Use(infraFactory.Middleware)
+			case types.OperationScope:
+				atomicGroup.Use(operationFactory.Middleware)
 			case types.ReleaseScope:
 				atomicGroup.Use(releaseFactory.Middleware)
 			}

+ 7 - 6
api/server/shared/apierrors/errors.go

@@ -5,7 +5,7 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/apierrors/alerter"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/logger"
 )
@@ -97,7 +97,8 @@ type ErrorOpts struct {
 }
 
 func HandleAPIError(
-	config *config.Config,
+	l *logger.Logger,
+	alerter alerter.Alerter,
 	w http.ResponseWriter,
 	r *http.Request,
 	err RequestError,
@@ -107,7 +108,7 @@ func HandleAPIError(
 	extErrorStr := err.ExternalError()
 
 	// log the internal error
-	event := config.Logger.Warn().
+	event := l.Warn().
 		Str("internal_error", err.InternalError()).
 		Str("external_error", extErrorStr)
 
@@ -117,11 +118,11 @@ func HandleAPIError(
 	event.Send()
 
 	// if the status code is internal server error, use alerter
-	if err.GetStatusCode() == http.StatusInternalServerError && config.Alerter != nil {
+	if err.GetStatusCode() == http.StatusInternalServerError && alerter != nil {
 		data["method"] = r.Method
 		data["url"] = r.URL.String()
 
-		config.Alerter.SendAlert(r.Context(), err, data)
+		alerter.SendAlert(r.Context(), err, data)
 	}
 
 	if writeErr {
@@ -140,7 +141,7 @@ func HandleAPIError(
 		writerErr := json.NewEncoder(w).Encode(resp)
 
 		if writerErr != nil {
-			event := config.Logger.Error().
+			event := l.Error().
 				Err(writerErr)
 
 			logger.AddLoggingContextScopes(r.Context(), event)

+ 1 - 1
api/server/shared/apitest/request.go

@@ -65,7 +65,7 @@ func (f *failingDecoderValidator) DecodeAndValidate(
 	r *http.Request,
 	v interface{},
 ) (ok bool) {
-	apierrors.HandleAPIError(f.config, w, r, apierrors.NewErrInternal(fmt.Errorf("fake error")), true)
+	apierrors.HandleAPIError(f.config.Logger, f.config.Alerter, w, r, apierrors.NewErrInternal(fmt.Errorf("fake error")), true)
 	return false
 }
 

+ 3 - 4
api/server/shared/config/config.go

@@ -10,12 +10,12 @@ import (
 	"github.com/porter-dev/porter/internal/billing"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
 	"github.com/porter-dev/porter/internal/integrations/powerdns"
-	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/notifier"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository/credentials"
+	"github.com/porter-dev/porter/provisioner/client"
 	"golang.org/x/oauth2"
 	"gorm.io/gorm"
 )
@@ -75,9 +75,8 @@ type Config struct {
 	// URLCache contains a cache of chart names to chart repos
 	URLCache *urlcache.ChartURLCache
 
-	// ProvisionerAgent is the kubernetes client responsible for creating new provisioner
-	// jobs
-	ProvisionerAgent *kubernetes.Agent
+	// ProvisionerClient is an authenticated client for the provisioner service
+	ProvisionerClient *client.Client
 
 	// DB is the gorm DB instance
 	DB *gorm.DB

+ 4 - 9
api/server/shared/config/env/envconfs.go

@@ -65,12 +65,9 @@ type ServerConf struct {
 	DOClientID     string `env:"DO_CLIENT_ID"`
 	DOClientSecret string `env:"DO_CLIENT_SECRET"`
 
-	// Options for the provisioner jobs
-	ProvisionerImageTag        string `env:"PROV_IMAGE_TAG,default=latest"`
-	ProvisionerImagePullSecret string `env:"PROV_IMAGE_PULL_SECRET"`
-	ProvisionerJobNamespace    string `env:"PROV_JOB_NAMESPACE,default=default"`
-	ProvisionerBackendURL      string `env:"PROV_BACKEND_URL"`
-	ProvisionerCredExchangeURL string `env:"PROV_CRED_EXCHANGE_URL,default=http://porter:8080"`
+	// Options for the provisioner service
+	ProvisionerServerURL string `env:"PROVISIONER_SERVER_URL"`
+	ProvisionerToken     string `env:"PROVISIONER_TOKEN"`
 
 	SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"`
 
@@ -86,9 +83,7 @@ type ServerConf struct {
 	SentryDSN string `env:"SENTRY_DSN"`
 	SentryEnv string `env:"SENTRY_ENV,default=dev"`
 
-	ProvisionerCluster string `env:"PROVISIONER_CLUSTER"`
-	IngressCluster     string `env:"INGRESS_CLUSTER"`
-	SelfKubeconfig     string `env:"SELF_KUBECONFIG"`
+	InitInCluster bool `env:"INIT_IN_CLUSTER,default=false"`
 
 	WelcomeFormWebhook string `env:"WELCOME_FORM_WEBHOOK"`
 

+ 12 - 19
api/server/shared/config/loader/loader.go

@@ -18,13 +18,12 @@ import (
 	"github.com/porter-dev/porter/internal/billing"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
 	"github.com/porter-dev/porter/internal/integrations/powerdns"
-	"github.com/porter-dev/porter/internal/kubernetes"
-	"github.com/porter-dev/porter/internal/kubernetes/local"
 	"github.com/porter-dev/porter/internal/notifier"
 	"github.com/porter-dev/porter/internal/notifier/sendgrid"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository/credentials"
 	"github.com/porter-dev/porter/internal/repository/gorm"
+	"github.com/porter-dev/porter/provisioner/client"
 
 	lr "github.com/porter-dev/porter/internal/logger"
 
@@ -73,7 +72,7 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 	res.Metadata = config.MetadataFromConf(envConf.ServerConf, e.version)
 	res.DB = InstanceDB
 
-	err = gorm.AutoMigrate(InstanceDB)
+	err = gorm.AutoMigrate(InstanceDB, sc.Debug)
 
 	if err != nil {
 		return nil, err
@@ -203,17 +202,13 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 
 	res.URLCache = urlcache.Init(sc.DefaultApplicationHelmRepoURL, sc.DefaultAddonHelmRepoURL)
 
-	provAgent, err := getProvisionerAgent(sc)
+	provClient, err := getProvisionerServiceClient(sc)
 
 	if err != nil {
 		return nil, err
 	}
 
-	res.ProvisionerAgent = provAgent
-
-	if res.ProvisionerAgent != nil && res.RedisConf.Enabled {
-		res.Metadata.Provisioning = true
-	}
+	res.ProvisionerClient = provClient
 
 	res.AnalyticsClient = analytics.InitializeAnalyticsSegmentClient(sc.SegmentClientKey, res.Logger)
 
@@ -224,20 +219,18 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 	return res, nil
 }
 
-func getProvisionerAgent(sc *env.ServerConf) (*kubernetes.Agent, error) {
-	if sc.ProvisionerCluster == "kubeconfig" && sc.SelfKubeconfig != "" {
-		agent, err := local.GetSelfAgentFromFileConfig(sc.SelfKubeconfig)
+func getProvisionerServiceClient(sc *env.ServerConf) (*client.Client, error) {
+	if sc.ProvisionerServerURL != "" && sc.ProvisionerToken != "" {
+		baseURL := fmt.Sprintf("%s/api/v1", sc.ProvisionerServerURL)
+
+		pClient, err := client.NewClient(baseURL, sc.ProvisionerToken, 0)
 
 		if err != nil {
-			return nil, fmt.Errorf("could not get in-cluster agent: %v", err)
+			return nil, err
 		}
 
-		return agent, nil
-	} else if sc.ProvisionerCluster == "kubeconfig" {
-		return nil, fmt.Errorf(`"kubeconfig" cluster option requires path to kubeconfig`)
+		return pClient, nil
 	}
 
-	agent, _ := kubernetes.GetAgentInClusterConfig()
-
-	return agent, nil
+	return nil, fmt.Errorf("required env vars not set for provisioner")
 }

+ 4 - 3
api/server/shared/config/metadata.go

@@ -1,6 +1,8 @@
 package config
 
-import "github.com/porter-dev/porter/api/server/shared/config/env"
+import (
+	"github.com/porter-dev/porter/api/server/shared/config/env"
+)
 
 type Metadata struct {
 	Provisioning       bool   `json:"provisioner"`
@@ -16,8 +18,7 @@ type Metadata struct {
 
 func MetadataFromConf(sc *env.ServerConf, version string) *Metadata {
 	return &Metadata{
-		// note: provisioning is set in the metadata after the loader is called
-		Provisioning:       false,
+		Provisioning:       sc.ProvisionerServerURL != "",
 		Github:             hasGithubAppVars(sc),
 		GithubLogin:        sc.GithubClientID != "" && sc.GithubClientSecret != "" && sc.GithubLoginEnabled,
 		BasicLogin:         sc.BasicLoginEnabled,

+ 2 - 2
api/server/shared/endpoints.go

@@ -23,8 +23,8 @@ type APIObjectEndpointFactory struct {
 }
 
 func NewAPIObjectEndpointFactory(conf *config.Config) APIEndpointFactory {
-	decoderValidator := NewDefaultRequestDecoderValidator(conf)
-	resultWriter := NewDefaultResultWriter(conf)
+	decoderValidator := NewDefaultRequestDecoderValidator(conf.Logger, conf.Alerter)
+	resultWriter := NewDefaultResultWriter(conf.Logger, conf.Alerter)
 
 	return &APIObjectEndpointFactory{
 		DecoderValidator: decoderValidator,

+ 9 - 6
api/server/shared/reader.go

@@ -5,8 +5,9 @@ import (
 	"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/server/shared/apierrors/alerter"
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/internal/logger"
 )
 
 type RequestDecoderValidator interface {
@@ -15,18 +16,20 @@ type RequestDecoderValidator interface {
 }
 
 type DefaultRequestDecoderValidator struct {
-	config    *config.Config
+	logger    *logger.Logger
+	alerter   alerter.Alerter
 	validator requestutils.Validator
 	decoder   requestutils.Decoder
 }
 
 func NewDefaultRequestDecoderValidator(
-	conf *config.Config,
+	logger *logger.Logger,
+	alerter alerter.Alerter,
 ) RequestDecoderValidator {
 	validator := requestutils.NewDefaultValidator()
 	decoder := requestutils.NewDefaultDecoder()
 
-	return &DefaultRequestDecoderValidator{conf, validator, decoder}
+	return &DefaultRequestDecoderValidator{logger, alerter, validator, decoder}
 }
 
 func (j *DefaultRequestDecoderValidator) DecodeAndValidate(
@@ -38,13 +41,13 @@ func (j *DefaultRequestDecoderValidator) DecodeAndValidate(
 
 	// decode the request parameters (body and query)
 	if requestErr = j.decoder.Decode(v, r); requestErr != nil {
-		apierrors.HandleAPIError(j.config, w, r, requestErr, true)
+		apierrors.HandleAPIError(j.logger, j.alerter, w, r, requestErr, true)
 		return false
 	}
 
 	// validate the request object
 	if requestErr = j.validator.Validate(v); requestErr != nil {
-		apierrors.HandleAPIError(j.config, w, r, requestErr, true)
+		apierrors.HandleAPIError(j.logger, j.alerter, w, r, requestErr, true)
 		return false
 	}
 

+ 10 - 5
api/server/shared/writer.go

@@ -7,7 +7,8 @@ import (
 	"syscall"
 
 	"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/apierrors/alerter"
+	"github.com/porter-dev/porter/internal/logger"
 )
 
 type ResultWriter interface {
@@ -17,11 +18,15 @@ type ResultWriter interface {
 // default generalizes response codes for common operations
 // (http.StatusOK, http.StatusCreated, etc)
 type DefaultResultWriter struct {
-	config *config.Config
+	logger  *logger.Logger
+	alerter alerter.Alerter
 }
 
-func NewDefaultResultWriter(conf *config.Config) ResultWriter {
-	return &DefaultResultWriter{conf}
+func NewDefaultResultWriter(
+	logger *logger.Logger,
+	alerter alerter.Alerter,
+) ResultWriter {
+	return &DefaultResultWriter{logger, alerter}
 }
 
 func (j *DefaultResultWriter) WriteResult(w http.ResponseWriter, r *http.Request, v interface{}) {
@@ -32,6 +37,6 @@ func (j *DefaultResultWriter) WriteResult(w http.ResponseWriter, r *http.Request
 		// the server was sending bytes.
 		return
 	} else if err != nil {
-		apierrors.HandleAPIError(j.config, w, r, apierrors.NewErrInternal(err), true)
+		apierrors.HandleAPIError(j.logger, j.alerter, w, r, apierrors.NewErrInternal(err), true)
 	}
 }

+ 11 - 5
api/types/database.go

@@ -11,11 +11,17 @@ type Database struct {
 
 	ClusterID uint `json:"cluster_id"`
 
-	InstanceID       string `json:"instance_id"`
-	InstanceEndpoint string `json:"instance_endpoint"`
-	InstanceName     string `json:"instance_name"`
-
-	Status string `json:"status"`
+	InstanceID        string `json:"instance_id"`
+	InstanceEndpoint  string `json:"instance_endpoint"`
+	InstanceName      string `json:"instance_name"`
+	InstanceStatus    string `json:"instance_status"`
+	InstanceDBFamily  string `json:"instance_db_family"`
+	InstanceDBVersion string `json:"instance_db_version"`
+	Status            string `json:"status"`
 }
 
 type ListDatabaseResponse []*Database
+
+type UpdateDatabaseStatusRequest struct {
+	Status string `json:"status" form:"required,oneof=destroying updating"`
+}

+ 1 - 0
api/types/form.go

@@ -52,6 +52,7 @@ type FormYAML struct {
 	Icon                string     `yaml:"icon" json:"icon"`
 	HasSource           string     `yaml:"hasSource" json:"hasSource"`
 	IncludeHiddenFields string     `yaml:"includeHiddenFields,omitempty" json:"includeHiddenFields,omitempty"`
+	IsClusterScoped     bool       `yaml:"isClusterScoped" json:"isClusterScoped"`
 	Description         string     `yaml:"description" json:"description"`
 	Tags                []string   `yaml:"tags" json:"tags"`
 	Tabs                []*FormTab `yaml:"tabs" json:"tabs,omitempty"`

+ 6 - 0
api/types/helm_repo.go

@@ -13,3 +13,9 @@ type HelmRepo struct {
 }
 
 type GetHelmRepoResponse HelmRepo
+
+type CreateHelmRepoRequest struct {
+	URL                string `json:"url"`
+	Name               string `json:"name" form:"required"`
+	BasicIntegrationID uint   `json:"basic_integration_id"`
+}

+ 72 - 0
api/types/infra.go

@@ -39,6 +39,10 @@ type Infra struct {
 	// The project that this integration belongs to
 	ProjectID uint `json:"project_id"`
 
+	APIVersion    string `json:"api_version,omitempty"`
+	SourceLink    string `json:"source_link,omitempty"`
+	SourceVersion string `json:"source_version,omitempty"`
+
 	// The type of infra that was provisioned
 	Kind InfraKind `json:"kind"`
 
@@ -59,4 +63,72 @@ type Infra struct {
 	// this is a map[string]string since we marshal into env vars anyway, but
 	// eventually this config will be more complex.
 	LastApplied map[string]string `json:"last_applied"`
+
+	// LatestOperation is the last operation that was run against this infra, if
+	// one exists
+	LatestOperation *Operation `json:"latest_operation"`
+}
+
+type InfraCredentials struct {
+	AWSIntegrationID uint `json:"aws_integration_id,omitempty"`
+	GCPIntegrationID uint `json:"gcp_integration_id,omitempty"`
+	DOIntegrationID  uint `json:"do_integration_id,omitempty"`
+}
+
+type CreateInfraRequest struct {
+	*InfraCredentials
+
+	ClusterID uint                   `json:"cluster_id"`
+	Kind      string                 `json:"kind" form:"required"`
+	Values    map[string]interface{} `json:"values" form:"required"`
+}
+
+type ListInfraRequest struct {
+	Version string `schema:"version"`
+}
+
+type DeleteInfraRequest struct {
+	*InfraCredentials
+}
+
+type RetryInfraRequest struct {
+	// Integration IDs are not required -- if they are passed in, they will override the
+	// existing integration IDs
+	*InfraCredentials
+
+	// Values are not required -- if they are not passed in, the values will be
+	// automatically populated from the previous operation
+	Values map[string]interface{} `json:"values"`
+}
+
+type OperationMeta struct {
+	LastUpdated time.Time `json:"last_updated"`
+	UID         string    `json:"id"`
+	InfraID     uint      `json:"infra_id"`
+	Type        string    `json:"type"`
+	Status      string    `json:"status"`
+	Errored     bool      `json:"errored"`
+	Error       string    `json:"error"`
+}
+
+type Operation struct {
+	*OperationMeta
+
+	LastApplied map[string]interface{} `json:"last_applied"`
+	Form        *FormYAML              `json:"form"`
+}
+
+type InfraTemplateMeta struct {
+	Icon               string `json:"icon"`
+	Description        string `json:"description"`
+	Name               string `json:"name"`
+	Version            string `json:"version"`
+	Kind               string `json:"kind"`
+	RequiredCredential string `json:"required_credential"`
+}
+
+type InfraTemplate struct {
+	*InfraTemplateMeta
+
+	Form *FormYAML `json:"form"`
 }

+ 9 - 0
api/types/namespace.go

@@ -176,3 +176,12 @@ type GetPreviousPodLogsRequest struct {
 type GetPreviousPodLogsResponse struct {
 	PrevLogs []string `json:"previous_logs"`
 }
+
+type GetJobsRequest struct {
+	Revision uint `schema:"revision"`
+}
+
+type GetJobRunsRequest struct {
+	Status string `schema:"status"`
+	Sort   string `schema:"sort"`
+}

+ 5 - 2
api/types/policy.go

@@ -10,6 +10,7 @@ const (
 	InviteScope          PermissionScope = "invite"
 	HelmRepoScope        PermissionScope = "helm_repo"
 	InfraScope           PermissionScope = "infra"
+	OperationScope       PermissionScope = "operation"
 	GitInstallationScope PermissionScope = "git_installation"
 	NamespaceScope       PermissionScope = "namespace"
 	SettingsScope        PermissionScope = "settings"
@@ -43,8 +44,10 @@ var ScopeHeirarchy = ScopeTree{
 		RegistryScope:        {},
 		HelmRepoScope:        {},
 		GitInstallationScope: {},
-		InfraScope:           {},
-		SettingsScope:        {},
+		InfraScope: {
+			OperationScope: {},
+		},
+		SettingsScope: {},
 	},
 }
 

+ 1 - 0
api/types/project.go

@@ -6,6 +6,7 @@ type Project struct {
 	Roles               []*Role `json:"roles"`
 	PreviewEnvsEnabled  bool    `json:"preview_envs_enabled"`
 	RDSDatabasesEnabled bool    `json:"enable_rds_databases"`
+	ManagedInfraEnabled bool    `json:"managed_infra_enabled"`
 }
 
 type CreateProjectRequest struct {

+ 2 - 184
api/types/provision.go

@@ -1,57 +1,11 @@
 package types
 
-import "strings"
-
-type CreateECRInfraRequest struct {
-	ECRName          string `json:"ecr_name" form:"required"`
-	ProjectID        uint   `json:"-" form:"required"`
-	AWSIntegrationID uint   `json:"aws_integration_id" form:"required"`
-}
-
-type CreateEKSInfraRequest struct {
-	EKSName          string `json:"eks_name" form:"required"`
-	MachineType      string `json:"machine_type"`
-	IssuerEmail      string `json:"issuer_email" form:"required"`
-	ProjectID        uint   `json:"-" form:"required"`
-	AWSIntegrationID uint   `json:"aws_integration_id" form:"required"`
-}
-
-type CreateGCRInfraRequest struct {
-	ProjectID        uint `json:"-" form:"required"`
-	GCPIntegrationID uint `json:"gcp_integration_id" form:"required"`
-}
-
-type CreateGKEInfraRequest struct {
-	GKEName          string `json:"gke_name" form:"required"`
-	GCPRegion        string `json:"gcp_region" form:"required"`
-	IssuerEmail      string `json:"issuer_email" form:"required"`
-	ProjectID        uint   `json:"-" form:"required"`
-	GCPIntegrationID uint   `json:"gcp_integration_id" form:"required"`
-}
-
-type CreateDOCRInfraRequest struct {
-	DOCRName             string `json:"docr_name" form:"required"`
-	DOCRSubscriptionTier string `json:"docr_subscription_tier" form:"required"`
-	ProjectID            uint   `json:"-" form:"required"`
-	DOIntegrationID      uint   `json:"do_integration_id" form:"required"`
-}
-
-type CreateDOKSInfraRequest struct {
-	DORegion        string `json:"do_region" form:"required"`
-	IssuerEmail     string `json:"issuer_email" form:"required"`
-	DOKSName        string `json:"doks_name" form:"required"`
-	ProjectID       uint   `json:"-" form:"required"`
-	DOIntegrationID uint   `json:"do_integration_id" form:"required"`
-}
-
 type CreateRDSInfraRequest struct {
 	// version of the postgres engine
 	DBEngineVersion string `json:"db_engine_version"`
-	// db type - postgress / mysql
-	DBFamily string `json:"db_family"`
 
-	// Deprecated, use DBEngineVersion instead
-	// PGVersion string `json:"pg_version"`
+	// db type - postgres / mysql
+	DBFamily string `json:"db_family"`
 
 	// db instance credentials specifications
 	DBName   string `json:"db_name"`
@@ -63,139 +17,3 @@ type CreateRDSInfraRequest struct {
 	DBMaxStorage string `json:"db_max_allocated_storage"`
 	DBEncryption bool   `json:"db_storage_encrypted"`
 }
-
-type RDSInfraLastApplied struct {
-	*CreateRDSInfraRequest
-
-	ClusterID uint   `json:"cluster_id"`
-	Namespace string `json:"namespace"`
-
-	AWSRegion            string
-	DBMajorEngineVersion string
-	DBStorageEncrypted   string
-	DeletionProtection   string
-	VPCID                string
-	Subnet1              string
-	Subnet2              string
-	Subnet3              string
-}
-
-type Family string
-
-type EngineVersion string
-
-func (e EngineVersion) MajorVersion() string {
-	semver := strings.Split(string(e), ".")
-
-	return strings.Join(semver[:len(semver)-1], ".")
-}
-
-type EngineVersions []EngineVersion
-
-func (e EngineVersions) VersionExists(version EngineVersion) bool {
-	for _, v := range e {
-		if version == v {
-			return true
-		}
-	}
-
-	return false
-}
-
-const (
-	FamilyPG9   Family = "postgres9"
-	FamilyPG10  Family = "postgres10"
-	FamilyPG11  Family = "postgres11"
-	FamilyPG12  Family = "postgres12"
-	FamilyPG13  Family = "postgres13"
-	FamilyMysql Family = "mysql"
-)
-
-var availablePG9Versions EngineVersions = EngineVersions{
-	"9.6.1",
-	"9.6.2",
-	"9.6.3",
-	"9.6.4",
-	"9.6.5",
-	"9.6.6",
-	"9.6.7",
-	"9.6.8",
-	"9.6.9",
-	"9.6.10",
-	"9.6.11",
-	"9.6.12",
-	"9.6.13",
-	"9.6.14",
-	"9.6.15",
-	"9.6.16",
-	"9.6.17",
-	"9.6.18",
-	"9.6.19",
-	"9.6.20",
-	"9.6.21",
-	"9.6.22",
-	"9.6.23",
-}
-
-var availablePG10Versions EngineVersions = EngineVersions{
-	"10.1",
-	"10.2",
-	"10.3",
-	"10.4",
-	"10.5",
-	"10.6",
-	"10.7",
-	"10.8",
-	"10.9",
-	"10.10",
-	"10.11",
-	"10.12",
-	"10.13",
-	"10.14",
-	"10.15",
-	"10.16",
-	"10.17",
-	"10.18",
-}
-
-var availablePG11Versions EngineVersions = EngineVersions{
-	"11.1",
-	"11.2",
-	"11.3",
-	"11.4",
-	"11.5",
-	"11.6",
-	"11.7",
-	"11.8",
-	"11.9",
-	"11.10",
-	"11.11",
-	"11.12",
-	"11.13",
-}
-
-var availablePG12Versions EngineVersions = EngineVersions{
-	"12.2",
-	"12.3",
-	"12.4",
-	"12.5",
-	"12.6",
-	"12.7",
-	"12.8",
-}
-
-var availablePG13Versions EngineVersions = EngineVersions{
-	"13.1",
-	"13.2",
-	"13.3",
-	"13.4",
-}
-
-var DBVersionMapping = map[Family]EngineVersions{
-	FamilyPG9:   availablePG9Versions,
-	FamilyPG10:  availablePG10Versions,
-	FamilyPG11:  availablePG11Versions,
-	FamilyPG12:  availablePG12Versions,
-	FamilyPG13:  availablePG13Versions,
-	FamilyMysql: {},
-}

+ 2 - 0
api/types/release.go

@@ -51,6 +51,8 @@ type CreateReleaseRequest struct {
 
 type CreateAddonRequest struct {
 	*CreateReleaseBaseRequest
+
+	HelmRepoID uint `json:"helm_repo_id"`
 }
 
 type RollbackReleaseRequest struct {

部分文件因文件數量過多而無法顯示