Răsfoiți Sursa

fix merge conflicts

Alexander Belanger 4 ani în urmă
părinte
comite
ff3cc08729
58 a modificat fișierele cu 2227 adăugiri și 964 ștergeri
  1. 3 0
      Makefile
  2. 24 0
      api/server/handlers/credentials/get_credentials_ce.go
  3. 21 0
      api/server/handlers/credentials/get_credentials_ee.go
  4. 158 92
      api/server/handlers/infra/delete.go
  5. 43 0
      api/server/handlers/infra/get_current.go
  6. 43 0
      api/server/handlers/infra/get_desired.go
  7. 68 0
      api/server/handlers/provision/helpers.go
  8. 39 19
      api/server/handlers/provision/provision_docr.go
  9. 39 19
      api/server/handlers/provision/provision_doks.go
  10. 38 17
      api/server/handlers/provision/provision_ecr.go
  11. 39 18
      api/server/handlers/provision/provision_eks.go
  12. 33 16
      api/server/handlers/provision/provision_gcr.go
  13. 38 17
      api/server/handlers/provision/provision_gke.go
  14. 26 0
      api/server/router/base.go
  15. 56 0
      api/server/router/infra.go
  16. 5 0
      api/server/shared/config/config.go
  17. 11 3
      api/server/shared/config/env/envconfs.go
  18. 9 0
      api/server/shared/config/loader/init_ee.go
  19. 9 6
      api/server/shared/config/loader/loader.go
  20. 1 1
      cmd/migrate/keyrotate/helpers_test.go
  21. 3 3
      cmd/migrate/keyrotate/rotate.go
  22. 3 3
      cmd/migrate/keyrotate/rotate_test.go
  23. 4 0
      cmd/migrate/main.go
  24. 12 0
      cmd/migrate/migrate_ce.go
  25. 39 0
      cmd/migrate/migrate_ee.go
  26. 142 0
      ee/api/server/handlers/credentials/get_credentials.go
  27. 17 0
      ee/api/types/cred_exchange.go
  28. 86 0
      ee/integrations/httpbackend/backend.go
  29. 50 0
      ee/integrations/httpbackend/types.go
  30. 72 0
      ee/integrations/vault/types.go
  31. 302 0
      ee/integrations/vault/vault.go
  32. 282 0
      ee/migrate/migrate_vault.go
  33. 9 273
      internal/kubernetes/agent.go
  34. 6 1
      internal/kubernetes/provisioner/aws/ecr/ecr.go
  35. 6 0
      internal/kubernetes/provisioner/aws/eks/eks.go
  36. 0 20
      internal/kubernetes/provisioner/do/do.go
  37. 0 30
      internal/kubernetes/provisioner/gcp/gcp.go
  38. 0 1
      internal/kubernetes/provisioner/input/docr.go
  39. 0 1
      internal/kubernetes/provisioner/input/doks.go
  40. 2 4
      internal/kubernetes/provisioner/input/ecr.go
  41. 2 5
      internal/kubernetes/provisioner/input/eks.go
  42. 2 3
      internal/kubernetes/provisioner/input/gcr.go
  43. 3 4
      internal/kubernetes/provisioner/input/gke.go
  44. 72 353
      internal/kubernetes/provisioner/provisioner.go
  45. 24 0
      internal/models/cred_exchange_token.go
  46. 8 0
      internal/repository/cred_exchange_token.go
  47. 46 0
      internal/repository/credentials/credentials.go
  48. 156 33
      internal/repository/gorm/auth.go
  49. 44 16
      internal/repository/gorm/auth_test.go
  50. 34 0
      internal/repository/gorm/cred_exchange_token.go
  51. 1 1
      internal/repository/gorm/helpers_test.go
  52. 1 0
      internal/repository/gorm/migrate.go
  53. 11 4
      internal/repository/gorm/repository.go
  54. 1 0
      internal/repository/repository.go
  55. 45 0
      internal/repository/test/cred_exchange_token.go
  56. 10 0
      internal/repository/test/repository.go
  57. 14 0
      scripts/dev-environment/RunMigrateDev.sh
  58. 15 1
      services/usage/usage.go

+ 3 - 0
Makefile

@@ -4,6 +4,9 @@ VERSION ?= dev
 start-dev: install setup-env-files
 	bash ./scripts/dev-environment/StartDevServer.sh
 
+run-migrate-dev: install setup-env-files
+	bash ./scripts/dev-environment/RunMigrateDev.sh
+
 install: 
 	bash ./scripts/dev-environment/SetupEnvironment.sh
 

+ 24 - 0
api/server/handlers/credentials/get_credentials_ce.go

@@ -0,0 +1,24 @@
+// +build !ee
+
+package credentials
+
+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"
+)
+
+type GetCredentialsHandler struct {
+	handlers.PorterHandlerReader
+	handlers.Unavailable
+}
+
+func NewGetCredentialsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) http.Handler {
+	return handlers.NewUnavailable(config, "get_credential")
+}

+ 21 - 0
api/server/handlers/credentials/get_credentials_ee.go

@@ -0,0 +1,21 @@
+// +build ee
+
+package credentials
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/ee/api/server/handlers/credentials"
+)
+
+var NewGetCredentialsHandler func(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) http.Handler
+
+func init() {
+	NewGetCredentialsHandler = credentials.NewCredentialsGetHandler
+}

+ 158 - 92
api/server/handlers/infra/delete.go

@@ -1,18 +1,23 @@
 package infra
 
 import (
+	"encoding/json"
 	"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"
 	"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/gke"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
 )
 
 type InfraDeleteHandler struct {
@@ -58,15 +63,15 @@ func (c *InfraDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	switch infra.Kind {
 	case types.InfraECR:
-		err = destroyECR(c.Repo(), c.Config(), infra, request.Name)
+		err = destroyECR(c.Config(), infra)
 	case types.InfraEKS:
-		err = destroyEKS(c.Repo(), c.Config(), infra, request.Name)
+		err = destroyEKS(c.Config(), infra)
 	case types.InfraDOCR:
-		err = destroyDOCR(c.Repo(), c.Config(), infra, request.Name)
+		err = destroyDOCR(c.Config(), infra)
 	case types.InfraDOKS:
-		err = destroyDOKS(c.Repo(), c.Config(), infra, request.Name)
+		err = destroyDOKS(c.Config(), infra)
 	case types.InfraGKE:
-		err = destroyGKE(c.Repo(), c.Config(), infra, request.Name)
+		err = destroyGKE(c.Config(), infra)
 	}
 
 	if err != nil {
@@ -75,132 +80,193 @@ func (c *InfraDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func destroyECR(repo repository.Repository, conf *config.Config, infra *models.Infra, name string) error {
-	awsInt, err := repo.AWSIntegration().ReadAWSIntegration(infra.ProjectID, infra.AWSIntegrationID)
+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
 	}
 
-	_, err = conf.ProvisionerAgent.ProvisionECR(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           infra.ProjectID,
-			Repo:                repo,
-			Infra:               infra,
-			Operation:           provisioner.Destroy,
-			PGConf:              conf.DBConf,
-			RedisConf:           conf.RedisConf,
-			ProvImageTag:        conf.ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: conf.ServerConf.ProvisionerImagePullSecret,
-		},
-		awsInt,
-		name,
-	)
+	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{
+		ECRName: lastAppliedECR.ECRName,
+	}
+	opts.OperationKind = provisioner.Destroy
+
+	err = conf.ProvisionerAgent.Provision(opts)
 
 	return err
 }
 
-func destroyEKS(repo repository.Repository, conf *config.Config, infra *models.Infra, name string) error {
-	awsInt, err := repo.AWSIntegration().ReadAWSIntegration(infra.ProjectID, infra.AWSIntegrationID)
+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
 	}
 
-	_, err = conf.ProvisionerAgent.ProvisionEKS(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           infra.ProjectID,
-			Repo:                repo,
-			Infra:               infra,
-			Operation:           provisioner.Destroy,
-			PGConf:              conf.DBConf,
-			RedisConf:           conf.RedisConf,
-			ProvImageTag:        conf.ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: conf.ServerConf.ProvisionerImagePullSecret,
-		},
-		awsInt,
-		name,
-		"",
-	)
+	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{
+		ClusterName: lastAppliedEKS.EKSName,
+		MachineType: lastAppliedEKS.MachineType,
+	}
+	opts.OperationKind = provisioner.Destroy
+
+	err = conf.ProvisionerAgent.Provision(opts)
 
 	return err
 }
 
-func destroyDOCR(repo repository.Repository, conf *config.Config, infra *models.Infra, name string) error {
-	doInt, err := repo.OAuthIntegration().ReadOAuthIntegration(infra.ProjectID, infra.DOIntegrationID)
+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)
 
 	if err != nil {
 		return err
 	}
 
-	_, err = conf.ProvisionerAgent.ProvisionDOCR(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           infra.ProjectID,
-			Repo:                repo,
-			Infra:               infra,
-			Operation:           provisioner.Destroy,
-			PGConf:              conf.DBConf,
-			RedisConf:           conf.RedisConf,
-			ProvImageTag:        conf.ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: conf.ServerConf.ProvisionerImagePullSecret,
-		},
-		doInt,
-		conf.DOConf,
-		name,
-		"",
-	)
+	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.DOCR = &docr.Conf{
+		DOCRName:             lastAppliedDOCR.DOCRName,
+		DOCRSubscriptionTier: lastAppliedDOCR.DOCRSubscriptionTier,
+	}
+
+	opts.OperationKind = provisioner.Destroy
+
+	err = conf.ProvisionerAgent.Provision(opts)
 
 	return err
 }
 
-func destroyDOKS(repo repository.Repository, conf *config.Config, infra *models.Infra, name string) error {
-	doInt, err := repo.OAuthIntegration().ReadOAuthIntegration(infra.ProjectID, infra.DOIntegrationID)
+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
+	}
+
+	doInt, err := conf.Repo.OAuthIntegration().ReadOAuthIntegration(infra.ProjectID, infra.DOIntegrationID)
 
 	if err != nil {
 		return err
 	}
 
-	_, err = conf.ProvisionerAgent.ProvisionDOKS(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           infra.ProjectID,
-			Repo:                repo,
-			Infra:               infra,
-			Operation:           provisioner.Destroy,
-			PGConf:              conf.DBConf,
-			RedisConf:           conf.RedisConf,
-			ProvImageTag:        conf.ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: conf.ServerConf.ProvisionerImagePullSecret,
-		},
-		doInt,
-		conf.DOConf,
-		"",
-		name,
-	)
+	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,
+	}
+
+	opts.OperationKind = provisioner.Destroy
+
+	err = conf.ProvisionerAgent.Provision(opts)
 
 	return err
 }
 
-func destroyGKE(repo repository.Repository, conf *config.Config, infra *models.Infra, name string) error {
-	gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(infra.ProjectID, infra.GCPIntegrationID)
+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
+	}
+
+	gcpInt, err := conf.Repo.GCPIntegration().ReadGCPIntegration(infra.ProjectID, infra.GCPIntegrationID)
 
 	if err != nil {
 		return err
 	}
 
-	_, err = conf.ProvisionerAgent.ProvisionGKE(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           infra.ProjectID,
-			Repo:                repo,
-			Infra:               infra,
-			Operation:           provisioner.Destroy,
-			PGConf:              conf.DBConf,
-			RedisConf:           conf.RedisConf,
-			ProvImageTag:        conf.ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: conf.ServerConf.ProvisionerImagePullSecret,
-		},
-		gcpInt,
-		name,
-	)
+	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{
+		ClusterName: lastAppliedGKE.GKEName,
+	}
+
+	opts.OperationKind = provisioner.Destroy
+
+	err = conf.ProvisionerAgent.Provision(opts)
 
 	return err
 }

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

@@ -0,0 +1,43 @@
+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/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 {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, current)
+}

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

@@ -0,0 +1,43 @@
+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/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 {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, desired)
+}

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

@@ -0,0 +1,68 @@
+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,
+		ProvImagePullSecret: conf.ServerConf.ProvisionerImagePullSecret,
+		TFHTTPBackendURL:    conf.ServerConf.ProvisionerBackendURL,
+		CredentialExchange: &provisioner.ProvisionCredentialExchange{
+			CredExchangeEndpoint: fmt.Sprintf("%s/api/internal/credentials", conf.ServerConf.ServerURL),
+			CredExchangeToken:    rawToken,
+			CredExchangeID:       ceToken.ID,
+		},
+	}, nil
+}

+ 39 - 19
api/server/handlers/provision/provision_docr.go

@@ -1,6 +1,7 @@
 package provision
 
 import (
+	"encoding/json"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -9,8 +10,8 @@ import (
 	"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"
 	"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"
@@ -43,7 +44,7 @@ func (c *ProvisionDOCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// get the AWS integration
+	// 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 {
@@ -63,6 +64,14 @@ func (c *ProvisionDOCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		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,
@@ -70,6 +79,7 @@ func (c *ProvisionDOCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Status:          types.StatusCreating,
 		DOIntegrationID: request.DOIntegrationID,
 		CreatedByUserID: user.ID,
+		LastApplied:     lastApplied,
 	}
 
 	// handle write to the database
@@ -80,23 +90,33 @@ func (c *ProvisionDOCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// launch provisioning pod
-	_, err = c.Config().ProvisionerAgent.ProvisionDOCR(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           proj.ID,
-			Repo:                c.Repo(),
-			Infra:               infra,
-			Operation:           provisioner.Apply,
-			PGConf:              c.Config().DBConf,
-			RedisConf:           c.Config().RedisConf,
-			ProvImageTag:        c.Config().ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: c.Config().ServerConf.ProvisionerImagePullSecret,
-		},
-		doInt,
-		c.Config().DOConf,
-		request.DOCRName,
-		request.DOCRSubscriptionTier,
-	)
+	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

+ 39 - 19
api/server/handlers/provision/provision_doks.go

@@ -1,6 +1,7 @@
 package provision
 
 import (
+	"encoding/json"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -9,8 +10,8 @@ import (
 	"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"
 	"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"
@@ -43,7 +44,7 @@ func (c *ProvisionDOKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// get the AWS integration
+	// 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 {
@@ -63,6 +64,14 @@ func (c *ProvisionDOKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		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,
@@ -70,6 +79,7 @@ func (c *ProvisionDOKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Status:          types.StatusCreating,
 		DOIntegrationID: request.DOIntegrationID,
 		CreatedByUserID: user.ID,
+		LastApplied:     lastApplied,
 	}
 
 	// handle write to the database
@@ -80,23 +90,33 @@ func (c *ProvisionDOKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// launch provisioning pod
-	_, err = c.Config().ProvisionerAgent.ProvisionDOKS(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           proj.ID,
-			Repo:                c.Repo(),
-			Infra:               infra,
-			Operation:           provisioner.Apply,
-			PGConf:              c.Config().DBConf,
-			RedisConf:           c.Config().RedisConf,
-			ProvImageTag:        c.Config().ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: c.Config().ServerConf.ProvisionerImagePullSecret,
-		},
-		doInt,
-		c.Config().DOConf,
-		request.DORegion,
-		request.DOKSName,
-	)
+	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,
+	}
+
+	opts.OperationKind = provisioner.Apply
+
+	err = c.Config().ProvisionerAgent.Provision(opts)
 
 	if err != nil {
 		infra.Status = types.StatusError

+ 38 - 17
api/server/handlers/provision/provision_ecr.go

@@ -1,6 +1,7 @@
 package provision
 
 import (
+	"encoding/json"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -9,8 +10,8 @@ import (
 	"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"
 	"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"
@@ -43,7 +44,7 @@ func (c *ProvisionECRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// get the AWS integration
+	// 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 {
@@ -63,6 +64,14 @@ func (c *ProvisionECRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		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,
@@ -70,6 +79,7 @@ func (c *ProvisionECRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Status:           types.StatusCreating,
 		AWSIntegrationID: request.AWSIntegrationID,
 		CreatedByUserID:  user.ID,
+		LastApplied:      lastApplied,
 	}
 
 	// handle write to the database
@@ -80,21 +90,32 @@ func (c *ProvisionECRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// launch provisioning pod
-	_, err = c.Config().ProvisionerAgent.ProvisionECR(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           proj.ID,
-			Repo:                c.Repo(),
-			Infra:               infra,
-			Operation:           provisioner.Apply,
-			PGConf:              c.Config().DBConf,
-			RedisConf:           c.Config().RedisConf,
-			ProvImageTag:        c.Config().ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: c.Config().ServerConf.ProvisionerImagePullSecret,
-		},
-		awsInt,
-		request.ECRName,
-	)
+	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

+ 39 - 18
api/server/handlers/provision/provision_eks.go

@@ -1,6 +1,7 @@
 package provision
 
 import (
+	"encoding/json"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -9,8 +10,8 @@ import (
 	"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"
 	"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"
@@ -43,7 +44,7 @@ func (c *ProvisionEKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// get the AWS integration
+	// 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 {
@@ -63,6 +64,14 @@ func (c *ProvisionEKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		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,
@@ -70,6 +79,7 @@ func (c *ProvisionEKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Status:           types.StatusCreating,
 		AWSIntegrationID: request.AWSIntegrationID,
 		CreatedByUserID:  user.ID,
+		LastApplied:      lastApplied,
 	}
 
 	// handle write to the database
@@ -80,22 +90,33 @@ func (c *ProvisionEKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// launch provisioning pod
-	_, err = c.Config().ProvisionerAgent.ProvisionEKS(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           proj.ID,
-			Repo:                c.Repo(),
-			Infra:               infra,
-			Operation:           provisioner.Apply,
-			PGConf:              c.Config().DBConf,
-			RedisConf:           c.Config().RedisConf,
-			ProvImageTag:        c.Config().ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: c.Config().ServerConf.ProvisionerImagePullSecret,
-		},
-		awsInt,
-		request.EKSName,
-		request.MachineType,
-	)
+	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,
+	}
+	opts.OperationKind = provisioner.Apply
+
+	err = c.Config().ProvisionerAgent.Provision(opts)
 
 	if err != nil {
 		infra.Status = types.StatusError

+ 33 - 16
api/server/handlers/provision/provision_gcr.go

@@ -1,6 +1,7 @@
 package provision
 
 import (
+	"encoding/json"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -9,7 +10,6 @@ import (
 	"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"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
@@ -43,7 +43,7 @@ func (c *ProvisionGCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// get the AWS integration
+	// 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 {
@@ -63,6 +63,14 @@ func (c *ProvisionGCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		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,
@@ -70,6 +78,7 @@ func (c *ProvisionGCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Status:           types.StatusCreating,
 		GCPIntegrationID: request.GCPIntegrationID,
 		CreatedByUserID:  user.ID,
+		LastApplied:      lastApplied,
 	}
 
 	// handle write to the database
@@ -80,20 +89,28 @@ func (c *ProvisionGCRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// launch provisioning pod
-	_, err = c.Config().ProvisionerAgent.ProvisionGCR(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           proj.ID,
-			Repo:                c.Repo(),
-			Infra:               infra,
-			Operation:           provisioner.Apply,
-			PGConf:              c.Config().DBConf,
-			RedisConf:           c.Config().RedisConf,
-			ProvImageTag:        c.Config().ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: c.Config().ServerConf.ProvisionerImagePullSecret,
-		},
-		gcpInt,
-	)
+	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.OperationKind = provisioner.Apply
+
+	err = c.Config().ProvisionerAgent.Provision(opts)
 
 	if err != nil {
 		infra.Status = types.StatusError

+ 38 - 17
api/server/handlers/provision/provision_gke.go

@@ -1,6 +1,7 @@
 package provision
 
 import (
+	"encoding/json"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -9,8 +10,8 @@ import (
 	"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"
 	"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"
@@ -43,7 +44,7 @@ func (c *ProvisionGKEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// get the AWS integration
+	// 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 {
@@ -63,6 +64,14 @@ func (c *ProvisionGKEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		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,
@@ -70,6 +79,7 @@ func (c *ProvisionGKEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Status:           types.StatusCreating,
 		GCPIntegrationID: request.GCPIntegrationID,
 		CreatedByUserID:  user.ID,
+		LastApplied:      lastApplied,
 	}
 
 	// handle write to the database
@@ -80,21 +90,32 @@ func (c *ProvisionGKEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	// launch provisioning pod
-	_, err = c.Config().ProvisionerAgent.ProvisionGKE(
-		&kubernetes.SharedProvisionOpts{
-			ProjectID:           proj.ID,
-			Repo:                c.Repo(),
-			Infra:               infra,
-			Operation:           provisioner.Apply,
-			PGConf:              c.Config().DBConf,
-			RedisConf:           c.Config().RedisConf,
-			ProvImageTag:        c.Config().ServerConf.ProvisionerImageTag,
-			ProvImagePullSecret: c.Config().ServerConf.ProvisionerImagePullSecret,
-		},
-		gcpInt,
-		request.GKEName,
-	)
+	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{
+		ClusterName: request.GKEName,
+	}
+
+	opts.OperationKind = provisioner.Apply
+
+	err = c.Config().ProvisionerAgent.Provision(opts)
 
 	if err != nil {
 		infra.Status = types.StatusError

+ 26 - 0
api/server/router/base.go

@@ -2,6 +2,7 @@ package router
 
 import (
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/credentials"
 	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/handlers/healthcheck"
 	"github.com/porter-dev/porter/api/server/handlers/metadata"
@@ -485,5 +486,30 @@ func GetBaseRoutes(
 		Router:   r,
 	})
 
+	// GET /api/internal/credentials
+	getCredentialsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/internal/credentials",
+			},
+			Scopes: []types.PermissionScope{},
+		},
+	)
+
+	getCredentialsHandler := credentials.NewGetCredentialsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getCredentialsEndpoint,
+		Handler:  getCredentialsHandler,
+		Router:   r,
+	})
+
 	return routes
 }

+ 56 - 0
api/server/router/infra.go

@@ -136,6 +136,62 @@ func getInfraRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/infras/{infra_id}/current -> infra.NewInfraGetHandler
+	getCurrentEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/current",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InfraScope,
+			},
+		},
+	)
+
+	getCurrentHandler := infra.NewInfraGetCurrentHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getCurrentEndpoint,
+		Handler:  getCurrentHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/infras/{infra_id}/desired -> infra.NewInfraGetHandler
+	getDesiredEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/desired",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InfraScope,
+			},
+		},
+	)
+
+	getDesiredHandler := infra.NewInfraGetDesiredHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getDesiredEndpoint,
+		Handler:  getDesiredHandler,
+		Router:   r,
+	})
+
 	// DELETE /api/projects/{project_id}/infras/{infra_id} -> infra.NewInfraDeleteHandler
 	deleteEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 5 - 0
api/server/shared/config/config.go

@@ -15,6 +15,7 @@ import (
 	"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"
 	"golang.org/x/oauth2"
 	"gorm.io/gorm"
 )
@@ -92,6 +93,10 @@ type Config struct {
 
 	// PowerDNSClient is a client for PowerDNS, if the Porter instance supports vanity URLs
 	PowerDNSClient *powerdns.Client
+
+	// CredentialBackend is the backend for credential storage, if external cred storage (like Vault)
+	// is used
+	CredentialBackend credentials.CredentialStorage
 }
 
 type ConfigLoader interface {

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

@@ -53,11 +53,15 @@ type ServerConf struct {
 	IronPlansServerURL string `env:"IRON_PLANS_SERVER_URL"`
 	WhitelistedUsers   []uint `env:"WHITELISTED_USERS"`
 
-	DOClientID                 string `env:"DO_CLIENT_ID"`
-	DOClientSecret             string `env:"DO_CLIENT_SECRET"`
+	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"`
-	SegmentClientKey           string `env:"SEGMENT_CLIENT_KEY"`
+	ProvisionerBackendURL      string `env:"PROV_BACKEND_URL"`
+
+	SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"`
 
 	// PowerDNS client API key and the host of the PowerDNS API server
 	PowerDNSAPIServerURL string `env:"POWER_DNS_API_SERVER_URL"`
@@ -93,6 +97,10 @@ type DBConf struct {
 
 	SQLLite     bool   `env:"SQL_LITE,default=false"`
 	SQLLitePath string `env:"SQL_LITE_PATH,default=/porter/porter.db"`
+
+	VaultPrefix    string `env:"VAULT_PREFIX,default=production"`
+	VaultAPIKey    string `env:"VAULT_API_KEY"`
+	VaultServerURL string `env:"VAULT_SERVER_URL"`
 }
 
 // RedisConf is the redis config required for the provisioner container

+ 9 - 0
api/server/shared/config/loader/init_ee.go

@@ -4,6 +4,7 @@ package loader
 
 import (
 	eeBilling "github.com/porter-dev/porter/ee/billing"
+	"github.com/porter-dev/porter/ee/integrations/vault"
 	"github.com/porter-dev/porter/ee/models"
 	eeGorm "github.com/porter-dev/porter/ee/repository/gorm"
 	"github.com/porter-dev/porter/internal/billing"
@@ -38,4 +39,12 @@ func init() {
 	} else {
 		InstanceBillingManager = &billing.NoopBillingManager{}
 	}
+
+	if InstanceEnvConf.DBConf.VaultAPIKey != "" && InstanceEnvConf.DBConf.VaultServerURL != "" && InstanceEnvConf.DBConf.VaultPrefix != "" {
+		InstanceCredentialBackend = vault.NewClient(
+			InstanceEnvConf.DBConf.VaultServerURL,
+			InstanceEnvConf.DBConf.VaultAPIKey,
+			InstanceEnvConf.DBConf.VaultPrefix,
+		)
+	}
 }

+ 9 - 6
api/server/shared/config/loader/loader.go

@@ -23,6 +23,7 @@ import (
 	"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"
 
 	lr "github.com/porter-dev/porter/internal/logger"
@@ -33,6 +34,7 @@ import (
 var InstanceBillingManager billing.BillingManager
 var InstanceEnvConf *envloader.EnvConf
 var InstanceDB *pgorm.DB
+var InstanceCredentialBackend credentials.CredentialStorage
 
 type EnvConfigLoader struct {
 	version string
@@ -60,11 +62,12 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 	sc := envConf.ServerConf
 
 	res = &config.Config{
-		Logger:         lr.NewConsole(sc.Debug),
-		ServerConf:     sc,
-		DBConf:         envConf.DBConf,
-		RedisConf:      envConf.RedisConf,
-		BillingManager: InstanceBillingManager,
+		Logger:            lr.NewConsole(sc.Debug),
+		ServerConf:        sc,
+		DBConf:            envConf.DBConf,
+		RedisConf:         envConf.RedisConf,
+		BillingManager:    InstanceBillingManager,
+		CredentialBackend: InstanceCredentialBackend,
 	}
 
 	res.Metadata = config.MetadataFromConf(envConf.ServerConf, e.version)
@@ -82,7 +85,7 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		key[i] = b
 	}
 
-	res.Repo = gorm.NewRepository(InstanceDB, &key)
+	res.Repo = gorm.NewRepository(InstanceDB, &key, InstanceCredentialBackend)
 
 	// create the session store
 	res.Store, err = sessionstore.NewStore(

+ 1 - 1
cmd/migrate/keyrotate/helpers_test.go

@@ -93,7 +93,7 @@ func setupTestEnv(tester *tester, t *testing.T) {
 	tester.Key = &key
 	tester.DB = db
 
-	tester.repo = gorm.NewRepository(db, &key)
+	tester.repo = gorm.NewRepository(db, &key, nil)
 }
 
 func cleanup(tester *tester, t *testing.T) {

+ 3 - 3
cmd/migrate/keyrotate/rotate.go

@@ -574,7 +574,7 @@ func rotateOAuthIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	}
 
 	// cluster-scoped repository
-	repo := gorm.NewOAuthIntegrationRepository(db, oldKey).(*gorm.OAuthIntegrationRepository)
+	repo := gorm.NewOAuthIntegrationRepository(db, oldKey, nil).(*gorm.OAuthIntegrationRepository)
 
 	// iterate (count / stepSize) + 1 times using Limit and Offset
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
@@ -629,7 +629,7 @@ func rotateGCPIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	}
 
 	// cluster-scoped repository
-	repo := gorm.NewGCPIntegrationRepository(db, oldKey).(*gorm.GCPIntegrationRepository)
+	repo := gorm.NewGCPIntegrationRepository(db, oldKey, nil).(*gorm.GCPIntegrationRepository)
 
 	// iterate (count / stepSize) + 1 times using Limit and Offset
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
@@ -682,7 +682,7 @@ func rotateAWSIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	}
 
 	// cluster-scoped repository
-	repo := gorm.NewAWSIntegrationRepository(db, oldKey).(*gorm.AWSIntegrationRepository)
+	repo := gorm.NewAWSIntegrationRepository(db, oldKey, nil).(*gorm.AWSIntegrationRepository)
 
 	// iterate (count / stepSize) + 1 times using Limit and Offset
 	for i := 0; i < (int(count)/stepSize)+1; i++ {

+ 3 - 3
cmd/migrate/keyrotate/rotate_test.go

@@ -471,7 +471,7 @@ func TestOAuthIntegrationModelRotation(t *testing.T) {
 	}
 
 	// very all oauths decoded properly
-	repo := gorm.NewOAuthIntegrationRepository(tester.DB, &newKey).(*gorm.OAuthIntegrationRepository)
+	repo := gorm.NewOAuthIntegrationRepository(tester.DB, &newKey, nil).(*gorm.OAuthIntegrationRepository)
 
 	oauths := []*ints.OAuthIntegration{}
 
@@ -527,7 +527,7 @@ func TestGCPIntegrationModelRotation(t *testing.T) {
 	}
 
 	// very all gcps decoded properly
-	repo := gorm.NewGCPIntegrationRepository(tester.DB, &newKey).(*gorm.GCPIntegrationRepository)
+	repo := gorm.NewGCPIntegrationRepository(tester.DB, &newKey, nil).(*gorm.GCPIntegrationRepository)
 
 	gcps := []*ints.GCPIntegration{}
 
@@ -575,7 +575,7 @@ func TestAWSIntegrationModelRotation(t *testing.T) {
 	}
 
 	// very all awss decoded properly
-	repo := gorm.NewAWSIntegrationRepository(tester.DB, &newKey).(*gorm.AWSIntegrationRepository)
+	repo := gorm.NewAWSIntegrationRepository(tester.DB, &newKey, nil).(*gorm.AWSIntegrationRepository)
 
 	awss := []*ints.AWSIntegration{}
 

+ 4 - 0
cmd/migrate/main.go

@@ -60,6 +60,10 @@ func main() {
 			logger.Fatal().Err(err).Msg("key rotation failed")
 		}
 	}
+
+	if err := InstanceMigrate(db, envConf.DBConf); err != nil {
+		logger.Fatal().Err(err).Msg("vault migration failed")
+	}
 }
 
 type RotateConf struct {

+ 12 - 0
cmd/migrate/migrate_ce.go

@@ -0,0 +1,12 @@
+// +build !ee
+
+package main
+
+import (
+	"github.com/porter-dev/porter/api/server/shared/config/env"
+	"gorm.io/gorm"
+)
+
+func InstanceMigrate(db *gorm.DB, dbConf *env.DBConf) error {
+	return nil
+}

+ 39 - 0
cmd/migrate/migrate_ee.go

@@ -0,0 +1,39 @@
+// +build ee
+
+package main
+
+import (
+	"log"
+
+	"github.com/joeshaw/envdecode"
+	"github.com/porter-dev/porter/api/server/shared/config/env"
+	"github.com/porter-dev/porter/ee/migrate"
+	"gorm.io/gorm"
+)
+
+func InstanceMigrate(db *gorm.DB, dbConf *env.DBConf) error {
+	if shouldVaultRotate() {
+		if err := migrate.MigrateVault(db, dbConf); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+type VaultMigrateConf struct {
+	// we add a dummy field to avoid empty struct issue with envdecode
+	DummyField   string `env:"ASDF,default=asdf"`
+	VaultMigrate bool   `env:"VAULT_MIGRATE"`
+}
+
+func shouldVaultRotate() bool {
+	var c VaultMigrateConf
+
+	if err := envdecode.StrictDecode(&c); err != nil {
+		log.Fatalf("Failed to decode Vault migration conf: %s", err)
+		return false
+	}
+
+	return c.VaultMigrate
+}

+ 142 - 0
ee/api/server/handlers/credentials/get_credentials.go

@@ -0,0 +1,142 @@
+// +build ee
+
+package credentials
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/ee/api/types"
+	"github.com/porter-dev/porter/ee/integrations/vault"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository/credentials"
+	"github.com/porter-dev/porter/internal/repository/gorm"
+	"golang.org/x/crypto/bcrypt"
+)
+
+type CredentialsGetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewCredentialsGetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) http.Handler {
+	return &CredentialsGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *CredentialsGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// read the request to get the token id and hashed token
+	req := &types.CredentialsExchangeRequest{}
+
+	// populate the request from the headers
+	req.CredExchangeToken = r.Header.Get("X-Porter-Token")
+	tokID, err := strconv.ParseUint(r.Header.Get("X-Porter-Token-ID"), 10, 64)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	req.CredExchangeID = uint(tokID)
+	req.VaultToken = r.Header.Get("X-Vault-Token")
+
+	// read the access token in the header, check against DB
+	ceToken, err := c.Repo().CredentialsExchangeToken().ReadCredentialsExchangeToken(req.CredExchangeID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	// TODO: verify hashed token!!
+	if valid, err := verifyToken(req.CredExchangeToken, ceToken); !valid {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	resp := &types.CredentialsExchangeResponse{}
+	repo := c.Repo()
+
+	// if the request contains a vault token, use that vault token to construct a new repository
+	// that will query vault using the passed in token
+	if req.VaultToken != "" {
+		// read the vault token in the header, create new vault client with this token
+		conf := c.Config().DBConf
+		vaultClient := vault.NewClient(conf.VaultServerURL, req.VaultToken, conf.VaultPrefix)
+
+		var key [32]byte
+
+		for i, b := range []byte(conf.EncryptionKey) {
+			key[i] = b
+		}
+
+		// use this vault client for the repo
+		repo = gorm.NewRepository(c.Config().DB, &key, vaultClient)
+	}
+
+	if ceToken.DOCredentialID != 0 {
+		doInt, err := repo.OAuthIntegration().ReadOAuthIntegration(ceToken.ProjectID, ceToken.DOCredentialID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+			return
+		}
+
+		resp.DO = &credentials.OAuthCredential{
+			ClientID:     doInt.ClientID,
+			AccessToken:  doInt.AccessToken,
+			RefreshToken: doInt.RefreshToken,
+		}
+	} else if ceToken.GCPCredentialID != 0 {
+		gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(ceToken.ProjectID, ceToken.GCPCredentialID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+			return
+		}
+
+		resp.GCP = &credentials.GCPCredential{
+			GCPKeyData: gcpInt.GCPKeyData,
+		}
+	} else if ceToken.AWSCredentialID != 0 {
+		awsInt, err := repo.AWSIntegration().ReadAWSIntegration(ceToken.ProjectID, ceToken.AWSCredentialID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+			return
+		}
+
+		resp.AWS = &credentials.AWSCredential{
+			AWSAccessKeyID:     awsInt.AWSAccessKeyID,
+			AWSClusterID:       awsInt.AWSClusterID,
+			AWSSecretAccessKey: awsInt.AWSSecretAccessKey,
+			AWSSessionToken:    awsInt.AWSSessionToken,
+		}
+	}
+
+	// return the decrypted credentials
+	c.WriteResult(w, r, resp)
+}
+
+func verifyToken(reqToken string, ceToken *models.CredentialsExchangeToken) (bool, error) {
+	// make sure the token is still valid and has not expired
+	if ceToken.IsExpired() {
+		return false, fmt.Errorf("token is expired")
+	}
+
+	// make sure the token is correct
+	if err := bcrypt.CompareHashAndPassword([]byte(ceToken.Token), []byte(reqToken)); err != nil {
+		return false, fmt.Errorf("verify token failed: %s", err)
+	}
+
+	return true, nil
+}

+ 17 - 0
ee/api/types/cred_exchange.go

@@ -0,0 +1,17 @@
+package types
+
+import "github.com/porter-dev/porter/internal/repository/credentials"
+
+type CredentialsExchangeRequest struct {
+	CredExchangeID    uint
+	CredExchangeToken string
+
+	// (optional) Vault token, if required
+	VaultToken string
+}
+
+type CredentialsExchangeResponse struct {
+	DO  *credentials.OAuthCredential `json:"do,omitempty"`
+	GCP *credentials.GCPCredential   `json:"gcp,omitempty"`
+	AWS *credentials.AWSCredential   `json:"aws,omitempty"`
+}

+ 86 - 0
ee/integrations/httpbackend/backend.go

@@ -0,0 +1,86 @@
+package httpbackend
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"time"
+)
+
+type Client struct {
+	backendURL string
+
+	httpClient *http.Client
+}
+
+func NewClient(backendURL string) *Client {
+	httpClient := &http.Client{
+		Timeout: time.Minute,
+	}
+
+	return &Client{backendURL, httpClient}
+}
+
+func (c *Client) GetCurrentState(name string) (*TFState, error) {
+	resp := &TFState{}
+
+	err := c.getRequest(fmt.Sprintf("%s/%s/tfstate", c.backendURL, name), resp)
+
+	return resp, err
+}
+
+type GetDesiredStateResp struct {
+	Data *DesiredTFState `json:"data"`
+}
+
+func (c *Client) GetDesiredState(name string) (*DesiredTFState, error) {
+	resp := &GetDesiredStateResp{}
+
+	err := c.getRequest(fmt.Sprintf("%s/%s/state", c.backendURL, name), resp)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Data, nil
+}
+
+func (c *Client) getRequest(path string, dst interface{}) error {
+	req, err := http.NewRequest(
+		"GET",
+		path,
+		nil,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	req.Header.Set("Accept", "application/json; charset=utf-8")
+
+	res, err := c.httpClient.Do(req)
+
+	if err != nil {
+		return err
+	}
+
+	defer res.Body.Close()
+
+	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
+		resBytes, err := ioutil.ReadAll(res.Body)
+
+		if err != nil {
+			return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
+		}
+
+		return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
+	}
+
+	if dst != nil {
+		return json.NewDecoder(res.Body).Decode(dst)
+	}
+
+	return nil
+}

+ 50 - 0
ee/integrations/httpbackend/types.go

@@ -0,0 +1,50 @@
+package httpbackend
+
+type TerraformEvent string
+
+const (
+	PlannedChange TerraformEvent = "planned_change"
+	ChangeSummary TerraformEvent = "change_summary"
+	ApplyStart    TerraformEvent = "apply_start"
+	ApplyProgress TerraformEvent = "apply_progress"
+	ApplyComplete TerraformEvent = "apply_complete"
+)
+
+type DesiredTFState []Resource
+
+type TFLogLine struct {
+	Level     string         `json:"@level"`
+	Message   string         `json:"@message"`
+	Timestamp string         `json:"@timestamp"`
+	Type      TerraformEvent `json:"type"`
+	Change    Change         `json:"change"`
+	Changes   Changes        `json:"changes"`
+}
+
+type Change struct {
+	Resource Resource `json:"resource"`
+	Action   string   `json:"action"`
+}
+
+type Resource struct {
+	Addr         string `json:"addr"`
+	ResourceType string `json:"resource_type"`
+	ResourceName string `json:"resource_name"`
+	Provider     string `json:"implied_provider"`
+}
+
+type Changes struct {
+	Add       int    `json:"add"`
+	Change    int    `json:"change"`
+	Remove    int    `json:"remove"`
+	Operation string `json:"operation"`
+}
+
+type TFState struct {
+	Version          int         `json:"version"`
+	TerraformVersion string      `json:"terraform_version"`
+	Serial           int         `json:"serial"`
+	Lineage          string      `json:"lineage"`
+	Outputs          interface{} `json:"outputs"`
+	Resources        interface{} `json:"resources"`
+}

+ 72 - 0
ee/integrations/vault/types.go

@@ -0,0 +1,72 @@
+// +build ee
+
+package vault
+
+import "github.com/porter-dev/porter/internal/repository/credentials"
+
+type CreateVaultSecretRequest struct {
+	Data interface{} `json:"data"`
+}
+
+type VaultGetResponse struct {
+	RequestID string `json:"request_id"`
+}
+
+type VaultMetadata struct {
+	CreatedTime string `json:"created_time"`
+	Destroyed   bool   `json:"destroyed"`
+	Version     uint   `json:"version"`
+}
+
+type GetOAuthCredentialResponse struct {
+	*VaultGetResponse
+	Data *GetOAuthCredentialData `json:"data"`
+}
+
+type GetOAuthCredentialData struct {
+	Metadata *VaultMetadata               `json:"metadata"`
+	Data     *credentials.OAuthCredential `json:"data"`
+}
+
+type GetGCPCredentialResponse struct {
+	*VaultGetResponse
+	Data *GetGCPCredentialData `json:"data"`
+}
+
+type GetGCPCredentialData struct {
+	Metadata *VaultMetadata             `json:"metadata"`
+	Data     *credentials.GCPCredential `json:"data"`
+}
+
+type GetAWSCredentialResponse struct {
+	*VaultGetResponse
+	Data *GetAWSCredentialData `json:"data"`
+}
+
+type GetAWSCredentialData struct {
+	Metadata *VaultMetadata             `json:"metadata"`
+	Data     *credentials.AWSCredential `json:"data"`
+}
+
+type CreatePolicyRequest struct {
+	Policy string `json:"policy"`
+}
+
+type CreateTokenRequest struct {
+	Policies []string `json:"policies"`
+	Meta     Meta     `json:"meta"`
+	TTL      string   `json:"ttl"`
+}
+
+type Meta struct {
+	User string `json:"user"`
+}
+
+type CreateTokenResponse struct {
+	*VaultGetResponse
+	Auth *TokenAuth `json:"auth"`
+}
+
+type TokenAuth struct {
+	Token string `json:"client_token"`
+}

+ 302 - 0
ee/integrations/vault/vault.go

@@ -0,0 +1,302 @@
+// +build ee
+
+package vault
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository/credentials"
+)
+
+// Client contains an API client for IronPlans
+type Client struct {
+	apiKey       string
+	serverURL    string
+	secretPrefix string
+	httpClient   *http.Client
+}
+
+// NewClient creates a new billing API client
+func NewClient(serverURL, apiKey, secretPrefix string) *Client {
+	httpClient := &http.Client{
+		Timeout: time.Minute,
+	}
+
+	return &Client{apiKey, serverURL, secretPrefix, httpClient}
+}
+
+func (c *Client) WriteOAuthCredential(
+	oauthIntegration *integrations.OAuthIntegration,
+	data *credentials.OAuthCredential,
+) error {
+	reqData := &CreateVaultSecretRequest{
+		Data: data,
+	}
+
+	return c.postRequest(fmt.Sprintf("/v1/%s", c.getOAuthCredentialPath(oauthIntegration)), reqData, nil)
+}
+
+func (c *Client) GetOAuthCredential(oauthIntegration *integrations.OAuthIntegration) (*credentials.OAuthCredential, error) {
+	resp := &GetOAuthCredentialResponse{}
+
+	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getOAuthCredentialPath(oauthIntegration)), resp)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Data.Data, nil
+}
+
+func (c *Client) CreateOAuthToken(oauthIntegration *integrations.OAuthIntegration) (string, error) {
+	credPath := c.getOAuthCredentialPath(oauthIntegration)
+	policyName := fmt.Sprintf("access-%d-oauth-%d", oauthIntegration.ProjectID, oauthIntegration.ID)
+
+	return c.getToken(credPath, policyName)
+}
+
+func (c *Client) getOAuthCredentialPath(oauthIntegration *integrations.OAuthIntegration) string {
+	return fmt.Sprintf(
+		"kv/data/secret/%s/%d/oauth/%d",
+		c.secretPrefix,
+		oauthIntegration.ProjectID,
+		oauthIntegration.ID,
+	)
+}
+
+func (c *Client) WriteGCPCredential(
+	gcpIntegration *integrations.GCPIntegration,
+	data *credentials.GCPCredential) error {
+	reqData := &CreateVaultSecretRequest{
+		Data: data,
+	}
+
+	return c.postRequest(fmt.Sprintf("/v1/%s", c.getGCPCredentialPath(gcpIntegration)), reqData, nil)
+}
+
+func (c *Client) GetGCPCredential(gcpIntegration *integrations.GCPIntegration) (*credentials.GCPCredential, error) {
+	resp := &GetGCPCredentialResponse{}
+
+	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getGCPCredentialPath(gcpIntegration)), resp)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Data.Data, nil
+}
+
+func (c *Client) CreateGCPToken(gcpIntegration *integrations.GCPIntegration) (string, error) {
+	credPath := c.getGCPCredentialPath(gcpIntegration)
+	policyName := fmt.Sprintf("access-%d-gcp-%d", gcpIntegration.ProjectID, gcpIntegration.ID)
+
+	return c.getToken(credPath, policyName)
+}
+
+func (c *Client) getGCPCredentialPath(gcpIntegration *integrations.GCPIntegration) string {
+	return fmt.Sprintf(
+		"kv/data/secret/%s/%d/gcp/%d",
+		c.secretPrefix,
+		gcpIntegration.ProjectID,
+		gcpIntegration.ID,
+	)
+}
+
+func (c *Client) WriteAWSCredential(
+	awsIntegration *integrations.AWSIntegration,
+	data *credentials.AWSCredential) error {
+	reqData := &CreateVaultSecretRequest{
+		Data: data,
+	}
+
+	return c.postRequest(fmt.Sprintf("/v1/%s", c.getAWSCredentialPath(awsIntegration)), reqData, nil)
+}
+
+func (c *Client) GetAWSCredential(awsIntegration *integrations.AWSIntegration) (*credentials.AWSCredential, error) {
+	resp := &GetAWSCredentialResponse{}
+
+	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getAWSCredentialPath(awsIntegration)), resp)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Data.Data, nil
+}
+
+func (c *Client) CreateAWSToken(awsIntegration *integrations.AWSIntegration) (string, error) {
+	credPath := c.getAWSCredentialPath(awsIntegration)
+	policyName := fmt.Sprintf("access-%d-aws-%d", awsIntegration.ProjectID, awsIntegration.ID)
+
+	return c.getToken(credPath, policyName)
+}
+
+func (c *Client) getAWSCredentialPath(awsIntegration *integrations.AWSIntegration) string {
+	return fmt.Sprintf(
+		"kv/data/secret/%s/%d/aws/%d",
+		c.secretPrefix,
+		awsIntegration.ProjectID,
+		awsIntegration.ID,
+	)
+}
+
+const readOnlyPolicyTemplate = `path "%s" {
+  capabilities = ["read"]
+}`
+
+func (c *Client) getToken(credPath, policyName string) (string, error) {
+	policy := fmt.Sprintf(readOnlyPolicyTemplate, credPath)
+
+	policyReq := &CreatePolicyRequest{
+		Policy: policy,
+	}
+
+	err := c.postRequest(fmt.Sprintf("/v1/sys/policy/%s", policyName), policyReq, nil)
+
+	if err != nil {
+		return "", err
+	}
+
+	tokenReq := &CreateTokenRequest{
+		Policies: []string{policyName},
+		Meta: Meta{
+			User: policyName,
+		},
+		TTL: "6h",
+	}
+
+	tokenResp := &CreateTokenResponse{}
+
+	err = c.postRequest("/v1/auth/token/create", tokenReq, tokenResp)
+
+	if err != nil {
+		return "", err
+	}
+
+	return tokenResp.Auth.Token, nil
+}
+
+func (c *Client) postRequest(path string, data interface{}, dst interface{}) error {
+	return c.writeRequest("POST", path, data, dst)
+}
+
+func (c *Client) putRequest(path string, data interface{}, dst interface{}) error {
+	return c.writeRequest("PUT", path, data, dst)
+}
+
+func (c *Client) deleteRequest(path string, data interface{}, dst interface{}) error {
+	return c.writeRequest("DELETE", path, data, dst)
+}
+
+func (c *Client) getRequest(path string, dst interface{}) error {
+	reqURL, err := url.Parse(c.serverURL)
+
+	if err != nil {
+		return nil
+	}
+
+	reqURL.Path = path
+
+	req, err := http.NewRequest(
+		"GET",
+		reqURL.String(),
+		nil,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	req.Header.Set("Accept", "application/json; charset=utf-8")
+	req.Header.Set("X-Vault-Token", c.apiKey)
+
+	res, err := c.httpClient.Do(req)
+
+	if err != nil {
+		return err
+	}
+
+	defer res.Body.Close()
+
+	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
+		resBytes, err := ioutil.ReadAll(res.Body)
+
+		if err != nil {
+			return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
+		}
+
+		return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
+	}
+
+	if dst != nil {
+		return json.NewDecoder(res.Body).Decode(dst)
+	}
+
+	return nil
+}
+
+func (c *Client) writeRequest(method, path string, data interface{}, dst interface{}) error {
+	reqURL, err := url.Parse(c.serverURL)
+
+	if err != nil {
+		return nil
+	}
+
+	reqURL.Path = path
+
+	var strData []byte
+
+	if data != nil {
+		strData, err = json.Marshal(data)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	req, err := http.NewRequest(
+		method,
+		reqURL.String(),
+		strings.NewReader(string(strData)),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	req.Header.Set("Accept", "application/json; charset=utf-8")
+	req.Header.Set("X-Vault-Token", c.apiKey)
+
+	res, err := c.httpClient.Do(req)
+
+	if err != nil {
+		return err
+	}
+
+	defer res.Body.Close()
+
+	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
+		resBytes, err := ioutil.ReadAll(res.Body)
+
+		if err != nil {
+			return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
+		}
+
+		return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
+	}
+
+	if dst != nil {
+		return json.NewDecoder(res.Body).Decode(dst)
+	}
+
+	return nil
+}

+ 282 - 0
ee/migrate/migrate_vault.go

@@ -0,0 +1,282 @@
+// +build ee
+
+package migrate
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/api/server/shared/config/env"
+	"github.com/porter-dev/porter/ee/integrations/vault"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository/credentials"
+	"gorm.io/gorm"
+)
+
+// process 100 records at a time
+const stepSize = 100
+
+func MigrateVault(db *gorm.DB, dbConf *env.DBConf) error {
+	var vaultClient *vault.Client
+
+	if dbConf.VaultAPIKey != "" && dbConf.VaultServerURL != "" && dbConf.VaultPrefix != "" {
+		vaultClient = vault.NewClient(
+			dbConf.VaultServerURL,
+			dbConf.VaultAPIKey,
+			dbConf.VaultPrefix,
+		)
+	} else {
+		return fmt.Errorf("env variables not properly set for vault migration")
+	}
+
+	err := migrateOAuthIntegrationModel(db, vaultClient)
+
+	if err != nil {
+		fmt.Printf("failed on oauth migration: %v\n", err)
+
+		return err
+	}
+
+	err = migrateGCPIntegrationModel(db, vaultClient)
+
+	if err != nil {
+		fmt.Printf("failed on gcp migration: %v\n", err)
+
+		return err
+	}
+
+	err = migrateAWSIntegrationModel(db, vaultClient)
+
+	if err != nil {
+		fmt.Printf("failed on aws migration: %v\n", err)
+
+		return err
+	}
+
+	return nil
+}
+
+func migrateOAuthIntegrationModel(db *gorm.DB, client *vault.Client) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.OAuthIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// make a map of ids to errors -- we don't clear the integrations with errors
+	errors := make(map[uint]error)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		oauths := []*ints.OAuthIntegration{}
+
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&oauths).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, oauth := range oauths {
+			// Check if record already exists in vault client. If so, we don't write anything to vault,
+			// since we don't want to overwrite any data that's been written.
+			if resp, _ := client.GetOAuthCredential(oauth); resp != nil {
+				continue
+			}
+
+			// write the data to the vault client
+			if err := client.WriteOAuthCredential(oauth, &credentials.OAuthCredential{
+				ClientID:     oauth.ClientID,
+				AccessToken:  oauth.AccessToken,
+				RefreshToken: oauth.RefreshToken,
+			}); err != nil {
+				errors[oauth.ID] = err
+				fmt.Printf("oauth vault write error on ID %d: %v\n", oauth.ID, err)
+			}
+		}
+	}
+
+	fmt.Printf("migrated %d oauth integrations with %d errors\n", count, len(errors))
+
+	saveErrors := make(map[uint]error, 0)
+
+	// iterate a second time, clearing the data
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		oauths := []*ints.OAuthIntegration{}
+
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&oauths).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, oauth := range oauths {
+			if _, found := errors[oauth.ID]; !found {
+				// clear the data from the db, and save
+				oauth.ClientID = []byte{}
+				oauth.AccessToken = []byte{}
+				oauth.RefreshToken = []byte{}
+
+				if err := db.Save(oauth).Error; err != nil {
+					saveErrors[oauth.ID] = err
+				}
+			}
+		}
+	}
+
+	fmt.Printf("cleared %d oauth integrations with %d errors\n", count, len(saveErrors))
+
+	for saveErrorID, saveError := range saveErrors {
+		fmt.Printf("oauth save error on ID %d: %v\n", saveErrorID, saveError)
+	}
+
+	return nil
+}
+
+func migrateGCPIntegrationModel(db *gorm.DB, client *vault.Client) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.GCPIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// make a map of ids to errors -- we don't clear the integrations with errors
+	errors := make(map[uint]error)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		gcps := []*ints.GCPIntegration{}
+
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&gcps).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, gcp := range gcps {
+			// Check if record already exists in vault client. If so, we don't write anything to vault,
+			// since we don't want to overwrite any data that's been written.
+			if resp, _ := client.GetGCPCredential(gcp); resp != nil {
+				continue
+			}
+
+			// write the data to the vault client
+			if err := client.WriteGCPCredential(gcp, &credentials.GCPCredential{
+				GCPKeyData: gcp.GCPKeyData,
+			}); err != nil {
+				errors[gcp.ID] = err
+				fmt.Printf("gcp vault write error on ID %d: %v\n", gcp.ID, err)
+			}
+		}
+	}
+
+	fmt.Printf("migrated %d gcp integrations with %d errors\n", count, len(errors))
+
+	saveErrors := make(map[uint]error, 0)
+
+	// iterate a second time, clearing the data
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		gcps := []*ints.GCPIntegration{}
+
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&gcps).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, gcp := range gcps {
+			if _, found := errors[gcp.ID]; !found {
+				// clear the data from the db, and save
+				gcp.GCPKeyData = []byte{}
+
+				if err := db.Save(gcp).Error; err != nil {
+					saveErrors[gcp.ID] = err
+				}
+			}
+		}
+	}
+
+	fmt.Printf("cleared %d gcp integrations with %d errors\n", count, len(saveErrors))
+
+	for saveErrorID, saveError := range saveErrors {
+		fmt.Printf("gcp save error on ID %d: %v\n", saveErrorID, saveError)
+	}
+
+	return nil
+}
+
+func migrateAWSIntegrationModel(db *gorm.DB, client *vault.Client) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.AWSIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// make a map of ids to errors -- we don't clear the integrations with errors
+	errors := make(map[uint]error)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		awss := []*ints.AWSIntegration{}
+
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&awss).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, aws := range awss {
+			// Check if record already exists in vault client. If so, we don't write anything to vault,
+			// since we don't want to overwrite any data that's been written.
+			if resp, _ := client.GetAWSCredential(aws); resp != nil {
+				continue
+			}
+
+			// write the data to the vault client
+			if err := client.WriteAWSCredential(aws, &credentials.AWSCredential{
+				AWSAccessKeyID:     aws.AWSAccessKeyID,
+				AWSClusterID:       aws.AWSClusterID,
+				AWSSecretAccessKey: aws.AWSSecretAccessKey,
+				AWSSessionToken:    aws.AWSSessionToken,
+			}); err != nil {
+				errors[aws.ID] = err
+				fmt.Printf("aws vault write error on ID %d: %v\n", aws.ID, err)
+			}
+		}
+	}
+
+	fmt.Printf("migrated %d aws integrations with %d errors\n", count, len(errors))
+
+	saveErrors := make(map[uint]error, 0)
+
+	// iterate a second time, clearing the data
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		awss := []*ints.AWSIntegration{}
+
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&awss).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, aws := range awss {
+			if _, found := errors[aws.ID]; !found {
+				// clear the data from the db, and save
+				aws.AWSAccessKeyID = []byte{}
+				aws.AWSClusterID = []byte{}
+				aws.AWSSecretAccessKey = []byte{}
+				aws.AWSSessionToken = []byte{}
+
+				if err := db.Save(aws).Error; err != nil {
+					saveErrors[aws.ID] = err
+				}
+			}
+		}
+	}
+
+	fmt.Printf("cleared %d aws integrations with %d errors\n", count, len(saveErrors))
+
+	for saveErrorID, saveError := range saveErrors {
+		fmt.Printf("aws save error on ID %d: %v\n", saveErrorID, saveError)
+	}
+
+	return nil
+}

+ 9 - 273
internal/kubernetes/agent.go

@@ -15,20 +15,9 @@ import (
 
 	goerrors "errors"
 
-	"github.com/porter-dev/porter/api/server/shared/config/env"
 	"github.com/porter-dev/porter/api/server/shared/websocket"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws"
-	"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"
-	"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"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp/gke"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/models/integrations"
-	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/registry"
 	"github.com/porter-dev/porter/internal/repository"
 	"golang.org/x/oauth2"
@@ -990,277 +979,24 @@ func (a *Agent) StreamHelmReleases(namespace string, chartList []string, selecto
 	return a.RunWebsocketTask(run)
 }
 
-type SharedProvisionOpts struct {
-	ProjectID           uint
-	Repo                repository.Repository
-	Infra               *models.Infra
-	Operation           provisioner.ProvisionerOperation
-	PGConf              *env.DBConf
-	RedisConf           *env.RedisConf
-	ProvImageTag        string
-	ProvImagePullSecret string
-}
-
-// ProvisionECR spawns a new provisioning pod that creates an ECR instance
-func (a *Agent) ProvisionECR(
-	opts *SharedProvisionOpts,
-	awsConf *integrations.AWSIntegration,
-	ecrName string,
-) (*batchv1.Job, error) {
-	id := opts.Infra.GetUniqueName()
-	prov := &provisioner.Conf{
-		ID:                  id,
-		Name:                fmt.Sprintf("prov-%s-%s", id, string(opts.Operation)),
-		Kind:                provisioner.ECR,
-		Operation:           opts.Operation,
-		Redis:               opts.RedisConf,
-		Postgres:            opts.PGConf,
-		ProvisionerImageTag: opts.ProvImageTag,
-		ImagePullSecret:     opts.ProvImagePullSecret,
-		LastApplied:         opts.Infra.LastApplied,
-		AWS: &aws.Conf{
-			AWSRegion:          awsConf.AWSRegion,
-			AWSAccessKeyID:     string(awsConf.AWSAccessKeyID),
-			AWSSecretAccessKey: string(awsConf.AWSSecretAccessKey),
-		},
-		ECR: &ecr.Conf{
-			ECRName: ecrName,
-		},
-	}
-
-	return a.provision(prov, opts.Infra, opts.Repo)
-}
-
-// ProvisionEKS spawns a new provisioning pod that creates an EKS instance
-func (a *Agent) ProvisionEKS(
-	opts *SharedProvisionOpts,
-	awsConf *integrations.AWSIntegration,
-	eksName, machineType string,
-) (*batchv1.Job, error) {
-	id := opts.Infra.GetUniqueName()
-	prov := &provisioner.Conf{
-		ID:                  id,
-		Name:                fmt.Sprintf("prov-%s-%s", id, string(opts.Operation)),
-		Kind:                provisioner.EKS,
-		Operation:           opts.Operation,
-		Redis:               opts.RedisConf,
-		Postgres:            opts.PGConf,
-		ProvisionerImageTag: opts.ProvImageTag,
-		ImagePullSecret:     opts.ProvImagePullSecret,
-		LastApplied:         opts.Infra.LastApplied,
-		AWS: &aws.Conf{
-			AWSRegion:          awsConf.AWSRegion,
-			AWSAccessKeyID:     string(awsConf.AWSAccessKeyID),
-			AWSSecretAccessKey: string(awsConf.AWSSecretAccessKey),
-		},
-		EKS: &eks.Conf{
-			ClusterName: eksName,
-			MachineType: machineType,
-		},
-	}
-
-	return a.provision(prov, opts.Infra, opts.Repo)
-}
-
-// ProvisionGCR spawns a new provisioning pod that creates a GCR instance
-func (a *Agent) ProvisionGCR(
-	opts *SharedProvisionOpts,
-	gcpConf *integrations.GCPIntegration,
-) (*batchv1.Job, error) {
-	id := opts.Infra.GetUniqueName()
-	prov := &provisioner.Conf{
-		ID:                  id,
-		Name:                fmt.Sprintf("prov-%s-%s", id, string(opts.Operation)),
-		Kind:                provisioner.GCR,
-		Operation:           opts.Operation,
-		Redis:               opts.RedisConf,
-		Postgres:            opts.PGConf,
-		ProvisionerImageTag: opts.ProvImageTag,
-		ImagePullSecret:     opts.ProvImagePullSecret,
-		LastApplied:         opts.Infra.LastApplied,
-		GCP: &gcp.Conf{
-			GCPRegion:    gcpConf.GCPRegion,
-			GCPProjectID: gcpConf.GCPProjectID,
-			GCPKeyData:   string(gcpConf.GCPKeyData),
-		},
-	}
-
-	return a.provision(prov, opts.Infra, opts.Repo)
-}
-
-// ProvisionGKE spawns a new provisioning pod that creates a GKE instance
-func (a *Agent) ProvisionGKE(
-	opts *SharedProvisionOpts,
-	gcpConf *integrations.GCPIntegration,
-	gkeName string,
-) (*batchv1.Job, error) {
-	id := opts.Infra.GetUniqueName()
-	prov := &provisioner.Conf{
-		ID:                  id,
-		Name:                fmt.Sprintf("prov-%s-%s", id, string(opts.Operation)),
-		Kind:                provisioner.GKE,
-		Operation:           opts.Operation,
-		Redis:               opts.RedisConf,
-		Postgres:            opts.PGConf,
-		ProvisionerImageTag: opts.ProvImageTag,
-		ImagePullSecret:     opts.ProvImagePullSecret,
-		LastApplied:         opts.Infra.LastApplied,
-		GCP: &gcp.Conf{
-			GCPRegion:    gcpConf.GCPRegion,
-			GCPProjectID: gcpConf.GCPProjectID,
-			GCPKeyData:   string(gcpConf.GCPKeyData),
-		},
-		GKE: &gke.Conf{
-			ClusterName: gkeName,
-		},
-	}
-
-	return a.provision(prov, opts.Infra, opts.Repo)
-}
-
-// ProvisionDOCR spawns a new provisioning pod that creates a DOCR instance
-func (a *Agent) ProvisionDOCR(
-	opts *SharedProvisionOpts,
-	doConf *integrations.OAuthIntegration,
-	doAuth *oauth2.Config,
-	docrName, docrSubscriptionTier string,
-) (*batchv1.Job, error) {
-	// get the token
-	oauthInt, err := opts.Repo.OAuthIntegration().ReadOAuthIntegration(
-		opts.ProjectID,
-		opts.Infra.DOIntegrationID,
-	)
-
-	if err != nil {
-		return nil, err
-	}
-
-	tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, opts.Repo))
+func (a *Agent) Provision(
+	opts *provisioner.ProvisionOpts,
+) error {
+	// get the provisioner job template
+	job, err := provisioner.GetProvisionerJobTemplate(opts)
 
 	if err != nil {
-		return nil, err
-	}
-
-	id := opts.Infra.GetUniqueName()
-	prov := &provisioner.Conf{
-		ID:                  id,
-		Name:                fmt.Sprintf("prov-%s-%s", id, string(opts.Operation)),
-		Kind:                provisioner.DOCR,
-		Operation:           opts.Operation,
-		Redis:               opts.RedisConf,
-		Postgres:            opts.PGConf,
-		ProvisionerImageTag: opts.ProvImageTag,
-		ImagePullSecret:     opts.ProvImagePullSecret,
-		LastApplied:         opts.Infra.LastApplied,
-		DO: &do.Conf{
-			DOToken: tok,
-		},
-		DOCR: &docr.Conf{
-			DOCRName:             docrName,
-			DOCRSubscriptionTier: docrSubscriptionTier,
-		},
-	}
-
-	return a.provision(prov, opts.Infra, opts.Repo)
-}
-
-// ProvisionDOKS spawns a new provisioning pod that creates a DOKS instance
-func (a *Agent) ProvisionDOKS(
-	opts *SharedProvisionOpts,
-	doConf *integrations.OAuthIntegration,
-	doAuth *oauth2.Config,
-	doRegion, doksClusterName string,
-) (*batchv1.Job, error) {
-	// get the token
-	oauthInt, err := opts.Repo.OAuthIntegration().ReadOAuthIntegration(
-		opts.ProjectID,
-		opts.Infra.DOIntegrationID,
-	)
-
-	if err != nil {
-		return nil, err
-	}
-
-	tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, opts.Repo))
-
-	if err != nil {
-		return nil, err
-	}
-
-	id := opts.Infra.GetUniqueName()
-	prov := &provisioner.Conf{
-		ID:                  id,
-		Name:                fmt.Sprintf("prov-%s-%s", id, string(opts.Operation)),
-		Kind:                provisioner.DOKS,
-		Operation:           opts.Operation,
-		Redis:               opts.RedisConf,
-		Postgres:            opts.PGConf,
-		LastApplied:         opts.Infra.LastApplied,
-		ProvisionerImageTag: opts.ProvImageTag,
-		ImagePullSecret:     opts.ProvImagePullSecret,
-		DO: &do.Conf{
-			DOToken: tok,
-		},
-		DOKS: &doks.Conf{
-			DORegion:        doRegion,
-			DOKSClusterName: doksClusterName,
-		},
-	}
-
-	return a.provision(prov, opts.Infra, opts.Repo)
-}
-
-// ProvisionTest spawns a new provisioning pod that tests provisioning
-func (a *Agent) ProvisionTest(
-	opts *SharedProvisionOpts,
-) (*batchv1.Job, error) {
-	id := opts.Infra.GetUniqueName()
-
-	prov := &provisioner.Conf{
-		ID:                  id,
-		Name:                fmt.Sprintf("prov-%s-%s", id, string(opts.Operation)),
-		Operation:           opts.Operation,
-		Kind:                provisioner.Test,
-		Redis:               opts.RedisConf,
-		Postgres:            opts.PGConf,
-		ProvisionerImageTag: opts.ProvImageTag,
-		ImagePullSecret:     opts.ProvImagePullSecret,
-	}
-
-	return a.provision(prov, opts.Infra, opts.Repo)
-}
-
-func (a *Agent) provision(
-	prov *provisioner.Conf,
-	infra *models.Infra,
-	repo repository.Repository,
-) (*batchv1.Job, error) {
-	prov.Namespace = "default"
-
-	job, err := prov.GetProvisionerJobTemplate()
-
-	if err != nil {
-		return nil, err
+		return err
 	}
 
-	job, err = a.Clientset.BatchV1().Jobs(prov.Namespace).Create(
+	// apply the provisioner job template
+	_, err = a.Clientset.BatchV1().Jobs("default").Create(
 		context.TODO(),
 		job,
 		metav1.CreateOptions{},
 	)
 
-	if err != nil {
-		return nil, err
-	}
-
-	infra.LastApplied = prov.LastApplied
-	infra, err = repo.Infra().UpdateInfra(infra)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return job, nil
+	return err
 }
 
 // CreateImagePullSecrets will create the required image pull secrets and

+ 6 - 1
internal/kubernetes/provisioner/aws/ecr/ecr.go

@@ -4,11 +4,16 @@ import v1 "k8s.io/api/core/v1"
 
 // Conf is the ECR cluster config required for the provisioner
 type Conf struct {
-	ECRName string
+	AWSRegion, ECRName string
 }
 
 // AttachECREnv adds the relevant ECR env for the provisioner
 func (conf *Conf) AttachECREnv(env []v1.EnvVar) []v1.EnvVar {
+	env = append(env, v1.EnvVar{
+		Name:  "AWS_REGION",
+		Value: conf.AWSRegion,
+	})
+
 	env = append(env, v1.EnvVar{
 		Name:  "ECR_NAME",
 		Value: conf.ECRName,

+ 6 - 0
internal/kubernetes/provisioner/aws/eks/eks.go

@@ -4,12 +4,18 @@ import v1 "k8s.io/api/core/v1"
 
 // Conf is the EKS cluster config required for the provisioner
 type Conf struct {
+	AWSRegion   string
 	ClusterName string
 	MachineType string
 }
 
 // AttachEKSEnv adds the relevant EKS env for the provisioner
 func (conf *Conf) AttachEKSEnv(env []v1.EnvVar) []v1.EnvVar {
+	env = append(env, v1.EnvVar{
+		Name:  "AWS_REGION",
+		Value: conf.AWSRegion,
+	})
+
 	env = append(env, v1.EnvVar{
 		Name:  "EKS_CLUSTER_NAME",
 		Value: conf.ClusterName,

+ 0 - 20
internal/kubernetes/provisioner/do/do.go

@@ -1,20 +0,0 @@
-package do
-
-import (
-	v1 "k8s.io/api/core/v1"
-)
-
-// Conf is just a DO token
-type Conf struct {
-	DOToken string
-}
-
-// AttachDOEnv adds the relevant DO env for the provisioner
-func (conf *Conf) AttachDOEnv(env []v1.EnvVar) []v1.EnvVar {
-	env = append(env, v1.EnvVar{
-		Name:  "DO_TOKEN",
-		Value: conf.DOToken,
-	})
-
-	return env
-}

+ 0 - 30
internal/kubernetes/provisioner/gcp/gcp.go

@@ -1,30 +0,0 @@
-package gcp
-
-import (
-	v1 "k8s.io/api/core/v1"
-)
-
-// Conf wraps the GCP integration model
-type Conf struct {
-	GCPRegion, GCPProjectID, GCPKeyData string
-}
-
-// AttachGCPEnv adds the relevant AWS env for the provisioner
-func (conf *Conf) AttachGCPEnv(env []v1.EnvVar) []v1.EnvVar {
-	env = append(env, v1.EnvVar{
-		Name:  "GCP_REGION",
-		Value: conf.GCPRegion,
-	})
-
-	env = append(env, v1.EnvVar{
-		Name:  "GCP_CREDENTIALS",
-		Value: conf.GCPKeyData,
-	})
-
-	env = append(env, v1.EnvVar{
-		Name:  "GCP_PROJECT_ID",
-		Value: conf.GCPProjectID,
-	})
-
-	return env
-}

+ 0 - 1
internal/kubernetes/provisioner/input/docr.go

@@ -5,7 +5,6 @@ import (
 )
 
 type DOCR struct {
-	DOToken              string `json:"do_token"`
 	DOCRName             string `json:"docr_name"`
 	DOCRSubscriptionTier string `json:"docr_subscription_tier"`
 }

+ 0 - 1
internal/kubernetes/provisioner/input/doks.go

@@ -6,7 +6,6 @@ import (
 
 type DOKS struct {
 	DORegion    string `json:"do_region"`
-	DOToken     string `json:"do_token"`
 	ClusterName string `json:"cluster_name"`
 }
 

+ 2 - 4
internal/kubernetes/provisioner/input/ecr.go

@@ -5,10 +5,8 @@ import (
 )
 
 type ECR struct {
-	AWSRegion    string `json:"aws_region"`
-	AWSAccessKey string `json:"aws_access_key"`
-	AWSSecretKey string `json:"aws_secret_key"`
-	ECRName      string `json:"ecr_name"`
+	AWSRegion string `json:"aws_region"`
+	ECRName   string `json:"ecr_name"`
 }
 
 func (ecr *ECR) GetInput() ([]byte, error) {

+ 2 - 5
internal/kubernetes/provisioner/input/eks.go

@@ -5,11 +5,8 @@ import (
 )
 
 type EKS struct {
-	AWSRegion    string `json:"aws_region"`
-	AWSAccessKey string `json:"aws_access_key"`
-	AWSSecretKey string `json:"aws_secret_key"`
-	ClusterName  string `json:"cluster_name"`
-	MachineType  string `json:"machine_type"`
+	AWSRegion   string `json:"aws_region"`
+	ClusterName string `json:"cluster_name"`
 }
 
 func (eks *EKS) GetInput() ([]byte, error) {

+ 2 - 3
internal/kubernetes/provisioner/input/gcr.go

@@ -5,9 +5,8 @@ import (
 )
 
 type GCR struct {
-	GCPCredentials string `json:"gcp_credentials"`
-	GCPRegion      string `json:"gcp_region"`
-	GCPProjectID   string `json:"gcp_project_id"`
+	GCPRegion    string `json:"gcp_region"`
+	GCPProjectID string `json:"gcp_project_id"`
 }
 
 func (gcr *GCR) GetInput() ([]byte, error) {

+ 3 - 4
internal/kubernetes/provisioner/input/gke.go

@@ -5,10 +5,9 @@ import (
 )
 
 type GKE struct {
-	GCPCredentials string `json:"gcp_credentials"`
-	GCPRegion      string `json:"gcp_region"`
-	GCPProjectID   string `json:"gcp_project_id"`
-	ClusterName    string `json:"cluster_name"`
+	GCPRegion    string `json:"gcp_region"`
+	GCPProjectID string `json:"gcp_project_id"`
+	ClusterName  string `json:"cluster_name"`
 }
 
 func (gke *GKE) GetInput() ([]byte, error) {

+ 72 - 353
internal/kubernetes/provisioner/provisioner.go

@@ -3,67 +3,18 @@ package provisioner
 import (
 	"fmt"
 
-	batchv1 "k8s.io/api/batch/v1"
-	v1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	"github.com/porter-dev/porter/api/server/shared/config/env"
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws"
+	"github.com/porter-dev/porter/api/types"
 	"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"
 	"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/input"
-
-	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp/gke"
+	"github.com/porter-dev/porter/internal/models"
+	batchv1 "k8s.io/api/batch/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-// InfraOption is a type of infrastructure that can be provisioned
-type InfraOption string
-
-// The list of infra options
-const (
-	Test InfraOption = "test"
-	ECR  InfraOption = "ecr"
-	EKS  InfraOption = "eks"
-	GCR  InfraOption = "gcr"
-	GKE  InfraOption = "gke"
-	DOCR InfraOption = "docr"
-	DOKS InfraOption = "doks"
-)
-
-// Conf is the config required to start a provisioner container
-type Conf struct {
-	Kind                InfraOption
-	Name                string
-	Namespace           string
-	ID                  string
-	Redis               *env.RedisConf
-	Postgres            *env.DBConf
-	Operation           ProvisionerOperation
-	ProvisionerImageTag string
-	ImagePullSecret     string
-	LastApplied         []byte
-
-	// provider-specific configurations
-
-	// AWS
-	AWS *aws.Conf
-	ECR *ecr.Conf
-	EKS *eks.Conf
-
-	// GKE
-	GCP *gcp.Conf
-	GKE *gke.Conf
-
-	// DO
-	DO   *do.Conf
-	DOCR *docr.Conf
-	DOKS *doks.Conf
-}
-
 type ProvisionerOperation string
 
 const (
@@ -71,238 +22,68 @@ const (
 	Destroy ProvisionerOperation = "destroy"
 )
 
-// GetProvisionerJobTemplate returns the manifest that should be applied to
-// create a provisioning job
-func (conf *Conf) GetProvisionerJobTemplate() (*batchv1.Job, error) {
-	operation := string(conf.Operation)
-
-	if operation == "" {
-		operation = string(Apply)
-	}
-
-	env := make([]v1.EnvVar, 0)
-
-	env = conf.attachDefaultEnv(env)
+type ProvisionCredentialExchange struct {
+	CredExchangeEndpoint string
+	CredExchangeToken    string
+	CredExchangeID       uint
 
-	ttl := int32(3600)
+	VaultToken string
+}
 
-	backoffLimit := int32(1)
+type ProvisionOpts struct {
+	DryRun              bool
+	Infra               *models.Infra
+	ProvImageTag        string
+	ProvImagePullSecret string
+	TFHTTPBackendURL    string
+	CredentialExchange  *ProvisionCredentialExchange
+	OperationKind       ProvisionerOperation
+
+	// resource-specific opts
+	ECR  *ecr.Conf
+	EKS  *eks.Conf
+	GKE  *gke.Conf
+	DOCR *docr.Conf
+	DOKS *doks.Conf
+}
 
+func GetProvisionerJobTemplate(opts *ProvisionOpts) (*batchv1.Job, error) {
 	labels := map[string]string{
 		"app": "provisioner",
 	}
 
-	args := make([]string, 0)
-
-	switch conf.Kind {
-	case Test:
-		args = []string{operation, "test", "hello"}
-	case ECR:
-		args = []string{operation, "ecr"}
-
-		if len(conf.LastApplied) > 0 {
-			inputConf, err := input.GetECRInput(conf.LastApplied)
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.AWS.AWSAccessKeyID = inputConf.AWSAccessKey
-			conf.AWS.AWSSecretAccessKey = inputConf.AWSSecretKey
-			conf.AWS.AWSRegion = inputConf.AWSRegion
-			conf.ECR.ECRName = inputConf.ECRName
-		} else {
-			inputConf := &input.ECR{
-				AWSRegion:    conf.AWS.AWSRegion,
-				AWSAccessKey: conf.AWS.AWSAccessKeyID,
-				AWSSecretKey: conf.AWS.AWSSecretAccessKey,
-				ECRName:      conf.ECR.ECRName,
-			}
-
-			lastApplied, err := inputConf.GetInput()
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.LastApplied = lastApplied
-		}
-
-		env = conf.AWS.AttachAWSEnv(env)
-		env = conf.ECR.AttachECREnv(env)
-	case EKS:
-		args = []string{operation, "eks"}
-
-		if len(conf.LastApplied) > 0 {
-			inputConf, err := input.GetEKSInput(conf.LastApplied)
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.AWS.AWSAccessKeyID = inputConf.AWSAccessKey
-			conf.AWS.AWSSecretAccessKey = inputConf.AWSSecretKey
-			conf.AWS.AWSRegion = inputConf.AWSRegion
-			conf.EKS.ClusterName = inputConf.ClusterName
-		} else {
-			inputConf := &input.EKS{
-				AWSRegion:    conf.AWS.AWSRegion,
-				AWSAccessKey: conf.AWS.AWSAccessKeyID,
-				AWSSecretKey: conf.AWS.AWSSecretAccessKey,
-				ClusterName:  conf.EKS.ClusterName,
-				MachineType:  conf.EKS.MachineType,
-			}
-
-			lastApplied, err := inputConf.GetInput()
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.LastApplied = lastApplied
-		}
-
-		env = conf.AWS.AttachAWSEnv(env)
-		env = conf.EKS.AttachEKSEnv(env)
-	case GCR:
-		args = []string{operation, "gcr"}
-
-		if len(conf.LastApplied) > 0 {
-			inputConf, err := input.GetGCRInput(conf.LastApplied)
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.GCP.GCPKeyData = inputConf.GCPCredentials
-			conf.GCP.GCPRegion = inputConf.GCPRegion
-			conf.GCP.GCPProjectID = inputConf.GCPProjectID
-		} else {
-			inputConf := &input.GCR{
-				GCPCredentials: conf.GCP.GCPKeyData,
-				GCPRegion:      conf.GCP.GCPRegion,
-				GCPProjectID:   conf.GCP.GCPProjectID,
-			}
-
-			lastApplied, err := inputConf.GetInput()
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.LastApplied = lastApplied
-		}
-
-		env = conf.GCP.AttachGCPEnv(env)
-	case GKE:
-		args = []string{operation, "gke"}
-
-		if len(conf.LastApplied) > 0 {
-			inputConf, err := input.GetGKEInput(conf.LastApplied)
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.GCP.GCPKeyData = inputConf.GCPCredentials
-			conf.GCP.GCPRegion = inputConf.GCPRegion
-			conf.GCP.GCPProjectID = inputConf.GCPProjectID
-			conf.GKE.ClusterName = inputConf.ClusterName
-		} else {
-			inputConf := &input.GKE{
-				GCPCredentials: conf.GCP.GCPKeyData,
-				GCPRegion:      conf.GCP.GCPRegion,
-				GCPProjectID:   conf.GCP.GCPProjectID,
-				ClusterName:    conf.GKE.ClusterName,
-			}
-
-			lastApplied, err := inputConf.GetInput()
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.LastApplied = lastApplied
-		}
-
-		env = conf.GCP.AttachGCPEnv(env)
-		env = conf.GKE.AttachGKEEnv(env)
-	case DOCR:
-		args = []string{operation, "docr"}
-
-		if len(conf.LastApplied) > 0 {
-			inputConf, err := input.GetDOCRInput(conf.LastApplied)
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.DO.DOToken = inputConf.DOToken
-			conf.DOCR.DOCRSubscriptionTier = inputConf.DOCRSubscriptionTier
-			conf.DOCR.DOCRName = inputConf.DOCRName
-		} else {
-			inputConf := &input.DOCR{
-				DOToken:              conf.DO.DOToken,
-				DOCRSubscriptionTier: conf.DOCR.DOCRSubscriptionTier,
-				DOCRName:             conf.DOCR.DOCRName,
-			}
-
-			lastApplied, err := inputConf.GetInput()
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.LastApplied = lastApplied
-		}
-
-		env = conf.DO.AttachDOEnv(env)
-		env = conf.DOCR.AttachDOCREnv(env)
-	case DOKS:
-		args = []string{operation, "doks"}
-
-		if len(conf.LastApplied) > 0 {
-			inputConf, err := input.GetDOKSInput(conf.LastApplied)
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.DO.DOToken = inputConf.DOToken
-			conf.DOKS.DORegion = inputConf.DORegion
-			conf.DOKS.DOKSClusterName = inputConf.ClusterName
-		} else {
-			inputConf := &input.DOKS{
-				DOToken:     conf.DO.DOToken,
-				DORegion:    conf.DOKS.DORegion,
-				ClusterName: conf.DOKS.DOKSClusterName,
-			}
-
-			lastApplied, err := inputConf.GetInput()
-
-			if err != nil {
-				return nil, err
-			}
-
-			conf.LastApplied = lastApplied
-		}
+	ttl := int32(3600)
 
-		env = conf.DO.AttachDOEnv(env)
-		env = conf.DOKS.AttachDOKSEnv(env)
-	}
+	backoffLimit := int32(1)
 
 	imagePullSecrets := []v1.LocalObjectReference{}
 
-	if conf.ImagePullSecret != "" {
+	if opts.ProvImagePullSecret != "" {
 		imagePullSecrets = append(imagePullSecrets, v1.LocalObjectReference{
-			Name: conf.ImagePullSecret,
+			Name: opts.ProvImagePullSecret,
 		})
 	}
 
+	env := GetTFEnv(opts)
+
+	// add resource-specific env
+	switch opts.Infra.Kind {
+	case types.InfraECR:
+		env = opts.ECR.AttachECREnv(env)
+	case types.InfraEKS:
+		env = opts.EKS.AttachEKSEnv(env)
+	case types.InfraGKE:
+		env = opts.GKE.AttachGKEEnv(env)
+	case types.InfraDOCR:
+		env = opts.DOCR.AttachDOCREnv(env)
+	case types.InfraDOKS:
+		env = opts.DOKS.AttachDOKSEnv(env)
+	}
+
 	return &batchv1.Job{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      conf.Name,
-			Namespace: conf.Namespace,
+			Name:      opts.Infra.GetUniqueName(),
+			Namespace: "default",
 			Labels:    labels,
 		},
 		Spec: batchv1.JobSpec{
@@ -318,10 +99,13 @@ func (conf *Conf) GetProvisionerJobTemplate() (*batchv1.Job, error) {
 					Containers: []v1.Container{
 						{
 							Name:            "provisioner",
-							Image:           "gcr.io/porter-dev-273614/provisioner:" + conf.ProvisionerImageTag,
+							Image:           "gcr.io/porter-dev-273614/provisioner:" + opts.ProvImageTag,
 							ImagePullPolicy: v1.PullAlways,
-							Args:            args,
-							Env:             env,
+							Args: []string{
+								string(opts.OperationKind),
+								string(opts.Infra.Kind),
+							},
+							Env: env,
 						},
 					},
 				},
@@ -330,107 +114,42 @@ func (conf *Conf) GetProvisionerJobTemplate() (*batchv1.Job, error) {
 	}, nil
 }
 
-// GetRedisStreamID returns the stream id that should be used
-func (conf *Conf) GetRedisStreamID() string {
-	return conf.ID
-}
-
-// GetTFWorkspaceID returns the workspace id that should be used
-func (conf *Conf) GetTFWorkspaceID() string {
-	return conf.ID
-}
-
-// attaches the env variables required by all provisioner instances
-func (conf *Conf) attachDefaultEnv(env []v1.EnvVar) []v1.EnvVar {
-	env = conf.addRedisEnv(env)
-	env = conf.addPostgresEnv(env)
-	env = conf.addTFEnv(env)
-
-	return env
-}
-
-// adds the env variables required for the Redis stream
-func (conf *Conf) addRedisEnv(env []v1.EnvVar) []v1.EnvVar {
-	env = append(env, v1.EnvVar{
-		Name:  "REDIS_ENABLED",
-		Value: "true",
-	})
-
-	env = append(env, v1.EnvVar{
-		Name:  "REDIS_HOST",
-		Value: conf.Redis.Host,
-	})
-
-	env = append(env, v1.EnvVar{
-		Name:  "REDIS_PORT",
-		Value: conf.Redis.Port,
-	})
-
-	env = append(env, v1.EnvVar{
-		Name:  "REDIS_USER",
-		Value: conf.Redis.Username,
-	})
-
-	env = append(env, v1.EnvVar{
-		Name:  "REDIS_PASS",
-		Value: conf.Redis.Password,
-		// ValueFrom: &v1.EnvVarSource{
-		// 	SecretKeyRef: &v1.SecretKeySelector{
-		// 		LocalObjectReference: v1.LocalObjectReference{
-		// 			Name: "redis",
-		// 		},
-		// 		Key: "redis-password",
-		// 	},
-		// },
-	})
-
-	env = append(env, v1.EnvVar{
-		Name:  "REDIS_STREAM_ID",
-		Value: conf.GetRedisStreamID(),
-	})
-
-	return env
-}
+func GetTFEnv(opts *ProvisionOpts) []v1.EnvVar {
+	env := make([]v1.EnvVar, 0)
 
-// adds the env variables required for the PG backend
-func (conf *Conf) addPostgresEnv(env []v1.EnvVar) []v1.EnvVar {
 	env = append(env, v1.EnvVar{
-		Name:  "PG_HOST",
-		Value: conf.Postgres.Host,
+		Name:  "TF_DIR",
+		Value: "./terraform",
 	})
 
 	env = append(env, v1.EnvVar{
-		Name:  "PG_PORT",
-		Value: fmt.Sprintf("%d", conf.Postgres.Port),
+		Name:  "TF_ORG_ID",
+		Value: opts.Infra.GetUniqueName(),
 	})
 
 	env = append(env, v1.EnvVar{
-		Name:  "PG_USER",
-		Value: conf.Postgres.Username,
+		Name:  "TF_BACKEND_URL",
+		Value: opts.TFHTTPBackendURL,
 	})
 
 	env = append(env, v1.EnvVar{
-		Name:  "PG_PASS",
-		Value: conf.Postgres.Password,
+		Name:  "CRED_EXCHANGE_ENDPOINT",
+		Value: opts.CredentialExchange.CredExchangeEndpoint,
 	})
 
-	return env
-}
-
-func (conf *Conf) addTFEnv(env []v1.EnvVar) []v1.EnvVar {
 	env = append(env, v1.EnvVar{
-		Name:  "TF_DIR",
-		Value: "./terraform",
+		Name:  "CRED_EXCHANGE_ID",
+		Value: fmt.Sprintf("%d", opts.CredentialExchange.CredExchangeID),
 	})
 
 	env = append(env, v1.EnvVar{
-		Name:  "TF_PORTER_BACKEND",
-		Value: "postgres",
+		Name:  "CRED_EXCHANGE_TOKEN",
+		Value: opts.CredentialExchange.CredExchangeToken,
 	})
 
 	env = append(env, v1.EnvVar{
-		Name:  "TF_PORTER_WORKSPACE",
-		Value: conf.GetTFWorkspaceID(),
+		Name:  "VAULT_TOKEN",
+		Value: opts.CredentialExchange.VaultToken,
 	})
 
 	return env

+ 24 - 0
internal/models/cred_exchange_token.go

@@ -0,0 +1,24 @@
+package models
+
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type CredentialsExchangeToken struct {
+	gorm.Model
+
+	ProjectID uint
+	Token     []byte
+	Expiry    *time.Time
+
+	DOCredentialID  uint
+	AWSCredentialID uint
+	GCPCredentialID uint
+}
+
+func (t *CredentialsExchangeToken) IsExpired() bool {
+	timeLeft := t.Expiry.Sub(time.Now())
+	return timeLeft < 0
+}

+ 8 - 0
internal/repository/cred_exchange_token.go

@@ -0,0 +1,8 @@
+package repository
+
+import "github.com/porter-dev/porter/internal/models"
+
+type CredentialsExchangeTokenRepository interface {
+	CreateCredentialsExchangeToken(ceToken *models.CredentialsExchangeToken) (*models.CredentialsExchangeToken, error)
+	ReadCredentialsExchangeToken(id uint) (*models.CredentialsExchangeToken, error)
+}

+ 46 - 0
internal/repository/credentials/credentials.go

@@ -0,0 +1,46 @@
+package credentials
+
+import "github.com/porter-dev/porter/internal/models/integrations"
+
+type OAuthCredential struct {
+	// The ID issued to the client
+	ClientID []byte `json:"client_id"`
+
+	// The end-users's access token
+	AccessToken []byte `json:"access_token"`
+
+	// The end-user's refresh token
+	RefreshToken []byte `json:"refresh_token"`
+}
+
+type GCPCredential struct {
+	// KeyData for a service account for GCP connectors
+	GCPKeyData []byte `json:"gcp_key_data"`
+}
+
+type AWSCredential struct {
+	// The AWS cluster ID
+	// See https://github.com/kubernetes-sigs/aws-iam-authenticator#what-is-a-cluster-id
+	AWSClusterID []byte `json:"aws_cluster_id"`
+
+	// The AWS access key for this IAM user
+	AWSAccessKeyID []byte `json:"aws_access_key_id"`
+
+	// The AWS secret key for this IAM user
+	AWSSecretAccessKey []byte `json:"aws_secret_access_key"`
+
+	// An optional session token, if the user is assuming a role
+	AWSSessionToken []byte `json:"aws_session_token"`
+}
+
+type CredentialStorage interface {
+	WriteOAuthCredential(oauthIntegration *integrations.OAuthIntegration, data *OAuthCredential) error
+	GetOAuthCredential(oauthIntegration *integrations.OAuthIntegration) (*OAuthCredential, error)
+	CreateOAuthToken(oauthIntegration *integrations.OAuthIntegration) (string, error)
+	WriteGCPCredential(gcpIntegration *integrations.GCPIntegration, data *GCPCredential) error
+	GetGCPCredential(gcpIntegration *integrations.GCPIntegration) (*GCPCredential, error)
+	CreateGCPToken(gcpIntegration *integrations.GCPIntegration) (string, error)
+	WriteAWSCredential(awsIntegration *integrations.AWSIntegration, data *AWSCredential) error
+	GetAWSCredential(awsIntegration *integrations.AWSIntegration) (*AWSCredential, error)
+	CreateAWSToken(awsIntegration *integrations.AWSIntegration) (string, error)
+}

+ 156 - 33
internal/repository/gorm/auth.go

@@ -3,6 +3,7 @@ package gorm
 import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/repository/credentials"
 	"gorm.io/gorm"
 
 	ints "github.com/porter-dev/porter/internal/models/integrations"
@@ -83,10 +84,6 @@ func (repo *KubeIntegrationRepository) ListKubeIntegrationsByProjectID(
 		return nil, err
 	}
 
-	for _, ki := range kis {
-		repo.DecryptKubeIntegrationData(ki, repo.key)
-	}
-
 	return kis, nil
 }
 
@@ -303,10 +300,6 @@ func (repo *BasicIntegrationRepository) ListBasicIntegrationsByProjectID(
 		return nil, err
 	}
 
-	for _, basic := range basics {
-		repo.DecryptBasicIntegrationData(basic, repo.key)
-	}
-
 	return basics, nil
 }
 
@@ -443,10 +436,6 @@ func (repo *OIDCIntegrationRepository) ListOIDCIntegrationsByProjectID(
 		return nil, err
 	}
 
-	for _, oidc := range oidcs {
-		repo.DecryptOIDCIntegrationData(oidc, repo.key)
-	}
-
 	return oidcs, nil
 }
 
@@ -590,8 +579,9 @@ func (repo *OIDCIntegrationRepository) DecryptOIDCIntegrationData(
 
 // OAuthIntegrationRepository uses gorm.DB for querying the database
 type OAuthIntegrationRepository struct {
-	db  *gorm.DB
-	key *[32]byte
+	db             *gorm.DB
+	key            *[32]byte
+	storageBackend credentials.CredentialStorage
 }
 
 // NewOAuthIntegrationRepository returns a OAuthIntegrationRepository which uses
@@ -600,8 +590,9 @@ type OAuthIntegrationRepository struct {
 func NewOAuthIntegrationRepository(
 	db *gorm.DB,
 	key *[32]byte,
+	storageBackend credentials.CredentialStorage,
 ) repository.OAuthIntegrationRepository {
-	return &OAuthIntegrationRepository{db, key}
+	return &OAuthIntegrationRepository{db, key, storageBackend}
 }
 
 // CreateOAuthIntegration creates a new oauth auth mechanism
@@ -614,6 +605,19 @@ func (repo *OAuthIntegrationRepository) CreateOAuthIntegration(
 		return nil, err
 	}
 
+	// if storage backend is not nil, strip out credential data, which will be stored in credential
+	// storage backend after write to DB
+	var credentialData = &credentials.OAuthCredential{}
+
+	if repo.storageBackend != nil {
+		credentialData.AccessToken = am.AccessToken
+		credentialData.RefreshToken = am.RefreshToken
+		credentialData.ClientID = am.ClientID
+		am.AccessToken = []byte{}
+		am.RefreshToken = []byte{}
+		am.ClientID = []byte{}
+	}
+
 	project := &models.Project{}
 
 	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
@@ -630,6 +634,14 @@ func (repo *OAuthIntegrationRepository) CreateOAuthIntegration(
 		return nil, err
 	}
 
+	if repo.storageBackend != nil {
+		err = repo.storageBackend.WriteOAuthCredential(am, credentialData)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	return am, nil
 }
 
@@ -643,6 +655,18 @@ func (repo *OAuthIntegrationRepository) ReadOAuthIntegration(
 		return nil, err
 	}
 
+	if repo.storageBackend != nil {
+		credentialData, err := repo.storageBackend.GetOAuthCredential(oauth)
+
+		if err != nil {
+			return nil, err
+		}
+
+		oauth.AccessToken = credentialData.AccessToken
+		oauth.RefreshToken = credentialData.RefreshToken
+		oauth.ClientID = credentialData.ClientID
+	}
+
 	err := repo.DecryptOAuthIntegrationData(oauth, repo.key)
 
 	if err != nil {
@@ -663,10 +687,6 @@ func (repo *OAuthIntegrationRepository) ListOAuthIntegrationsByProjectID(
 		return nil, err
 	}
 
-	for _, oauth := range oauths {
-		repo.DecryptOAuthIntegrationData(oauth, repo.key)
-	}
-
 	return oauths, nil
 }
 
@@ -680,6 +700,19 @@ func (repo *OAuthIntegrationRepository) UpdateOAuthIntegration(
 		return nil, err
 	}
 
+	// if storage backend is not nil, strip out credential data, which will be stored in credential
+	// storage backend after write to DB
+	var credentialData = &credentials.OAuthCredential{}
+
+	if repo.storageBackend != nil {
+		credentialData.AccessToken = am.AccessToken
+		credentialData.RefreshToken = am.RefreshToken
+		credentialData.ClientID = am.ClientID
+		am.AccessToken = []byte{}
+		am.RefreshToken = []byte{}
+		am.ClientID = []byte{}
+	}
+
 	if err := repo.db.Save(am).Error; err != nil {
 		return nil, err
 	}
@@ -690,6 +723,14 @@ func (repo *OAuthIntegrationRepository) UpdateOAuthIntegration(
 		return nil, err
 	}
 
+	if repo.storageBackend != nil {
+		err = repo.storageBackend.WriteOAuthCredential(am, credentialData)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	return am, nil
 }
 
@@ -773,8 +814,9 @@ func (repo *OAuthIntegrationRepository) DecryptOAuthIntegrationData(
 
 // GCPIntegrationRepository uses gorm.DB for querying the database
 type GCPIntegrationRepository struct {
-	db  *gorm.DB
-	key *[32]byte
+	db             *gorm.DB
+	key            *[32]byte
+	storageBackend credentials.CredentialStorage
 }
 
 // NewGCPIntegrationRepository returns a GCPIntegrationRepository which uses
@@ -783,8 +825,9 @@ type GCPIntegrationRepository struct {
 func NewGCPIntegrationRepository(
 	db *gorm.DB,
 	key *[32]byte,
+	storageBackend credentials.CredentialStorage,
 ) repository.GCPIntegrationRepository {
-	return &GCPIntegrationRepository{db, key}
+	return &GCPIntegrationRepository{db, key, storageBackend}
 }
 
 // CreateGCPIntegration creates a new gcp auth mechanism
@@ -797,6 +840,15 @@ func (repo *GCPIntegrationRepository) CreateGCPIntegration(
 		return nil, err
 	}
 
+	// if storage backend is not nil, strip out credential data, which will be stored in credential
+	// storage backend after write to DB
+	var credentialData = &credentials.GCPCredential{}
+
+	if repo.storageBackend != nil {
+		credentialData.GCPKeyData = am.GCPKeyData
+		am.GCPKeyData = []byte{}
+	}
+
 	project := &models.Project{}
 
 	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
@@ -813,6 +865,14 @@ func (repo *GCPIntegrationRepository) CreateGCPIntegration(
 		return nil, err
 	}
 
+	if repo.storageBackend != nil {
+		err = repo.storageBackend.WriteGCPCredential(am, credentialData)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	return am, nil
 }
 
@@ -826,6 +886,16 @@ func (repo *GCPIntegrationRepository) ReadGCPIntegration(
 		return nil, err
 	}
 
+	if repo.storageBackend != nil {
+		credentialData, err := repo.storageBackend.GetGCPCredential(gcp)
+
+		if err != nil {
+			return nil, err
+		}
+
+		gcp.GCPKeyData = credentialData.GCPKeyData
+	}
+
 	err := repo.DecryptGCPIntegrationData(gcp, repo.key)
 
 	if err != nil {
@@ -846,10 +916,6 @@ func (repo *GCPIntegrationRepository) ListGCPIntegrationsByProjectID(
 		return nil, err
 	}
 
-	for _, gcp := range gcps {
-		repo.DecryptGCPIntegrationData(gcp, repo.key)
-	}
-
 	return gcps, nil
 }
 
@@ -893,8 +959,9 @@ func (repo *GCPIntegrationRepository) DecryptGCPIntegrationData(
 
 // AWSIntegrationRepository uses gorm.DB for querying the database
 type AWSIntegrationRepository struct {
-	db  *gorm.DB
-	key *[32]byte
+	db             *gorm.DB
+	key            *[32]byte
+	storageBackend credentials.CredentialStorage
 }
 
 // NewAWSIntegrationRepository returns a AWSIntegrationRepository which uses
@@ -903,8 +970,9 @@ type AWSIntegrationRepository struct {
 func NewAWSIntegrationRepository(
 	db *gorm.DB,
 	key *[32]byte,
+	storageBackend credentials.CredentialStorage,
 ) repository.AWSIntegrationRepository {
-	return &AWSIntegrationRepository{db, key}
+	return &AWSIntegrationRepository{db, key, storageBackend}
 }
 
 // CreateAWSIntegration creates a new aws auth mechanism
@@ -917,6 +985,21 @@ func (repo *AWSIntegrationRepository) CreateAWSIntegration(
 		return nil, err
 	}
 
+	// if storage backend is not nil, strip out credential data, which will be stored in credential
+	// storage backend after write to DB
+	var credentialData = &credentials.AWSCredential{}
+
+	if repo.storageBackend != nil {
+		credentialData.AWSAccessKeyID = am.AWSAccessKeyID
+		credentialData.AWSClusterID = am.AWSClusterID
+		credentialData.AWSSecretAccessKey = am.AWSSecretAccessKey
+		credentialData.AWSSessionToken = am.AWSSessionToken
+		am.AWSAccessKeyID = []byte{}
+		am.AWSClusterID = []byte{}
+		am.AWSSecretAccessKey = []byte{}
+		am.AWSSessionToken = []byte{}
+	}
+
 	project := &models.Project{}
 
 	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
@@ -933,6 +1016,14 @@ func (repo *AWSIntegrationRepository) CreateAWSIntegration(
 		return nil, err
 	}
 
+	if repo.storageBackend != nil {
+		err = repo.storageBackend.WriteAWSCredential(am, credentialData)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	return am, nil
 }
 
@@ -946,10 +1037,33 @@ func (repo *AWSIntegrationRepository) OverwriteAWSIntegration(
 		return nil, err
 	}
 
+	// if storage backend is not nil, strip out credential data, which will be stored in credential
+	// storage backend after write to DB
+	var credentialData = &credentials.AWSCredential{}
+
+	if repo.storageBackend != nil {
+		credentialData.AWSAccessKeyID = am.AWSAccessKeyID
+		credentialData.AWSClusterID = am.AWSClusterID
+		credentialData.AWSSecretAccessKey = am.AWSSecretAccessKey
+		credentialData.AWSSessionToken = am.AWSSessionToken
+		am.AWSAccessKeyID = []byte{}
+		am.AWSClusterID = []byte{}
+		am.AWSSecretAccessKey = []byte{}
+		am.AWSSessionToken = []byte{}
+	}
+
 	if err := repo.db.Save(am).Error; err != nil {
 		return nil, err
 	}
 
+	if repo.storageBackend != nil {
+		err = repo.storageBackend.WriteAWSCredential(am, credentialData)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	return am, nil
 }
 
@@ -963,6 +1077,19 @@ func (repo *AWSIntegrationRepository) ReadAWSIntegration(
 		return nil, err
 	}
 
+	if repo.storageBackend != nil {
+		credentialData, err := repo.storageBackend.GetAWSCredential(aws)
+
+		if err != nil {
+			return nil, err
+		}
+
+		aws.AWSAccessKeyID = credentialData.AWSAccessKeyID
+		aws.AWSClusterID = credentialData.AWSClusterID
+		aws.AWSSecretAccessKey = credentialData.AWSSecretAccessKey
+		aws.AWSSessionToken = credentialData.AWSSessionToken
+	}
+
 	err := repo.DecryptAWSIntegrationData(aws, repo.key)
 
 	if err != nil {
@@ -983,10 +1110,6 @@ func (repo *AWSIntegrationRepository) ListAWSIntegrationsByProjectID(
 		return nil, err
 	}
 
-	for _, aws := range awss {
-		repo.DecryptAWSIntegrationData(aws, repo.key)
-	}
-
 	return awss, nil
 }
 

+ 44 - 16
internal/repository/gorm/auth_test.go

@@ -81,7 +81,7 @@ func TestListKubeIntegrationsByProjectID(t *testing.T) {
 		Mechanism:  ints.KubeLocal,
 		ProjectID:  tester.initProjects[0].ID,
 		UserID:     tester.initUsers[0].ID,
-		Kubeconfig: []byte("current-context: testing\n"),
+		Kubeconfig: []byte(""),
 	}
 
 	ki := kis[0]
@@ -89,6 +89,9 @@ func TestListKubeIntegrationsByProjectID(t *testing.T) {
 	// reset fields for reflect.DeepEqual
 	ki.Model = orm.Model{}
 
+	// list methods don't decrypt, so don't check those
+	ki.Kubeconfig = []byte("")
+
 	if diff := deep.Equal(expKI, *ki); diff != nil {
 		t.Errorf("incorrect kube integration")
 		t.Error(diff)
@@ -166,8 +169,8 @@ func TestListBasicIntegrationsByProjectID(t *testing.T) {
 	expBasic := ints.BasicIntegration{
 		ProjectID: tester.initProjects[0].ID,
 		UserID:    tester.initUsers[0].ID,
-		Username:  []byte("username"),
-		Password:  []byte("password"),
+		Username:  []byte(""),
+		Password:  []byte(""),
 	}
 
 	basic := basics[0]
@@ -175,6 +178,10 @@ func TestListBasicIntegrationsByProjectID(t *testing.T) {
 	// reset fields for reflect.DeepEqual
 	basic.Model = orm.Model{}
 
+	// list methods don't decrypt, so don't check those
+	basic.Username = []byte("")
+	basic.Password = []byte("")
+
 	if diff := deep.Equal(expBasic, *basic); diff != nil {
 		t.Errorf("incorrect basic integration")
 		t.Error(diff)
@@ -257,11 +264,11 @@ func TestListOIDCIntegrationsByProjectID(t *testing.T) {
 		Client:       ints.OIDCKube,
 		ProjectID:    tester.initProjects[0].ID,
 		UserID:       tester.initUsers[0].ID,
-		IssuerURL:    []byte("https://oidc.example.com"),
-		ClientID:     []byte("exampleclientid"),
-		ClientSecret: []byte("exampleclientsecret"),
-		IDToken:      []byte("idtoken"),
-		RefreshToken: []byte("refreshtoken"),
+		IssuerURL:    []byte(""),
+		ClientID:     []byte(""),
+		ClientSecret: []byte(""),
+		IDToken:      []byte(""),
+		RefreshToken: []byte(""),
 	}
 
 	oidc := oidcs[0]
@@ -269,6 +276,13 @@ func TestListOIDCIntegrationsByProjectID(t *testing.T) {
 	// reset fields for reflect.DeepEqual
 	oidc.Model = orm.Model{}
 
+	// list methods don't decrypt, so don't check those
+	oidc.IssuerURL = []byte("")
+	oidc.ClientID = []byte("")
+	oidc.ClientSecret = []byte("")
+	oidc.IDToken = []byte("")
+	oidc.RefreshToken = []byte("")
+
 	if diff := deep.Equal(expOIDC, *oidc); diff != nil {
 		t.Errorf("incorrect oidc integration")
 		t.Error(diff)
@@ -349,9 +363,9 @@ func TestListOAuthIntegrationsByProjectID(t *testing.T) {
 	// make sure data is correct
 	expOAuth := ints.OAuthIntegration{
 		SharedOAuthModel: ints.SharedOAuthModel{
-			ClientID:     []byte("exampleclientid"),
-			AccessToken:  []byte("idtoken"),
-			RefreshToken: []byte("refreshtoken"),
+			ClientID:     []byte(""),
+			AccessToken:  []byte(""),
+			RefreshToken: []byte(""),
 		},
 		Client:    types.OAuthGithub,
 		ProjectID: tester.initProjects[0].ID,
@@ -363,6 +377,11 @@ func TestListOAuthIntegrationsByProjectID(t *testing.T) {
 	// reset fields for reflect.DeepEqual
 	oauth.Model = orm.Model{}
 
+	// list methods don't decrypt, so don't check those
+	oauth.SharedOAuthModel.ClientID = []byte("")
+	oauth.SharedOAuthModel.AccessToken = []byte("")
+	oauth.SharedOAuthModel.RefreshToken = []byte("")
+
 	if diff := deep.Equal(expOAuth, *oauth); diff != nil {
 		t.Errorf("incorrect oauth integration")
 		t.Error(diff)
@@ -443,7 +462,7 @@ func TestListGCPIntegrationsByProjectID(t *testing.T) {
 		UserID:       tester.initUsers[0].ID,
 		GCPProjectID: "test-proj-123456",
 		GCPUserEmail: "test@test.it",
-		GCPKeyData:   []byte("{\"test\":\"key\"}"),
+		GCPKeyData:   []byte(""),
 	}
 
 	gcp := gcps[0]
@@ -451,6 +470,9 @@ func TestListGCPIntegrationsByProjectID(t *testing.T) {
 	// reset fields for reflect.DeepEqual
 	gcp.Model = orm.Model{}
 
+	// list methods don't decrypt, so don't check those
+	gcp.GCPKeyData = []byte("")
+
 	if diff := deep.Equal(expGCP, *gcp); diff != nil {
 		t.Errorf("incorrect gcp integration")
 		t.Error(diff)
@@ -581,10 +603,10 @@ func TestListAWSIntegrationsByProjectID(t *testing.T) {
 	expAWS := ints.AWSIntegration{
 		ProjectID:          tester.initProjects[0].ID,
 		UserID:             tester.initUsers[0].ID,
-		AWSClusterID:       []byte("example-cluster-0"),
-		AWSAccessKeyID:     []byte("accesskey"),
-		AWSSecretAccessKey: []byte("secret"),
-		AWSSessionToken:    []byte("optional"),
+		AWSClusterID:       []byte(""),
+		AWSAccessKeyID:     []byte(""),
+		AWSSecretAccessKey: []byte(""),
+		AWSSessionToken:    []byte(""),
 	}
 
 	aws := awss[0]
@@ -592,6 +614,12 @@ func TestListAWSIntegrationsByProjectID(t *testing.T) {
 	// reset fields for reflect.DeepEqual
 	aws.Model = orm.Model{}
 
+	// list methods don't decrypt, so don't check those
+	aws.AWSClusterID = []byte("")
+	aws.AWSAccessKeyID = []byte("")
+	aws.AWSSecretAccessKey = []byte("")
+	aws.AWSSessionToken = []byte("")
+
 	if diff := deep.Equal(expAWS, *aws); diff != nil {
 		t.Errorf("incorrect aws integration")
 		t.Error(diff)

+ 34 - 0
internal/repository/gorm/cred_exchange_token.go

@@ -0,0 +1,34 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// CredentialsExchangeTokenRepository uses gorm.DB for querying the database
+type CredentialsExchangeTokenRepository struct {
+	db *gorm.DB
+}
+
+func NewCredentialsExchangeTokenRepository(db *gorm.DB) repository.CredentialsExchangeTokenRepository {
+	return &CredentialsExchangeTokenRepository{db}
+}
+
+func (repo *CredentialsExchangeTokenRepository) CreateCredentialsExchangeToken(ceToken *models.CredentialsExchangeToken) (*models.CredentialsExchangeToken, error) {
+	if err := repo.db.Create(ceToken).Error; err != nil {
+		return nil, err
+	}
+
+	return ceToken, nil
+}
+
+func (repo *CredentialsExchangeTokenRepository) ReadCredentialsExchangeToken(id uint) (*models.CredentialsExchangeToken, error) {
+	ceToken := &models.CredentialsExchangeToken{}
+
+	if err := repo.db.Where("id = ?", id).First(&ceToken).Error; err != nil {
+		return nil, err
+	}
+
+	return ceToken, nil
+}

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

@@ -89,7 +89,7 @@ func setupTestEnv(tester *tester, t *testing.T) {
 
 	tester.key = &key
 
-	tester.repo = gorm.NewRepository(db, &key)
+	tester.repo = gorm.NewRepository(db, &key, nil)
 }
 
 func cleanup(tester *tester, t *testing.T) {

+ 1 - 0
internal/repository/gorm/migrate.go

@@ -32,6 +32,7 @@ func AutoMigrate(db *gorm.DB) error {
 		&models.ProjectUsage{},
 		&models.ProjectUsageCache{},
 		&models.Onboarding{},
+		&models.CredentialsExchangeToken{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 11 - 4
internal/repository/gorm/repository.go

@@ -2,6 +2,7 @@ package gorm
 
 import (
 	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/repository/credentials"
 	"gorm.io/gorm"
 )
 
@@ -33,6 +34,7 @@ type GormRepository struct {
 	event                     repository.EventRepository
 	projectUsage              repository.ProjectUsageRepository
 	onboarding                repository.ProjectOnboardingRepository
+	ceToken                   repository.CredentialsExchangeTokenRepository
 }
 
 func (t *GormRepository) User() repository.UserRepository {
@@ -143,9 +145,13 @@ func (t *GormRepository) Onboarding() repository.ProjectOnboardingRepository {
 	return t.onboarding
 }
 
+func (t *GormRepository) CredentialsExchangeToken() repository.CredentialsExchangeTokenRepository {
+	return t.ceToken
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
-func NewRepository(db *gorm.DB, key *[32]byte) repository.Repository {
+func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.CredentialStorage) repository.Repository {
 	return &GormRepository{
 		user:                      NewUserRepository(db),
 		session:                   NewSessionRepository(db),
@@ -164,9 +170,9 @@ func NewRepository(db *gorm.DB, key *[32]byte) repository.Repository {
 		kubeIntegration:           NewKubeIntegrationRepository(db, key),
 		basicIntegration:          NewBasicIntegrationRepository(db, key),
 		oidcIntegration:           NewOIDCIntegrationRepository(db, key),
-		oauthIntegration:          NewOAuthIntegrationRepository(db, key),
-		gcpIntegration:            NewGCPIntegrationRepository(db, key),
-		awsIntegration:            NewAWSIntegrationRepository(db, key),
+		oauthIntegration:          NewOAuthIntegrationRepository(db, key, storageBackend),
+		gcpIntegration:            NewGCPIntegrationRepository(db, key, storageBackend),
+		awsIntegration:            NewAWSIntegrationRepository(db, key, storageBackend),
 		githubAppInstallation:     NewGithubAppInstallationRepository(db),
 		githubAppOAuthIntegration: NewGithubAppOAuthIntegrationRepository(db),
 		slackIntegration:          NewSlackIntegrationRepository(db, key),
@@ -174,5 +180,6 @@ func NewRepository(db *gorm.DB, key *[32]byte) repository.Repository {
 		event:                     NewEventRepository(db),
 		projectUsage:              NewProjectUsageRepository(db),
 		onboarding:                NewProjectOnboardingRepository(db),
+		ceToken:                   NewCredentialsExchangeTokenRepository(db),
 	}
 }

+ 1 - 0
internal/repository/repository.go

@@ -28,4 +28,5 @@ type Repository interface {
 	Event() EventRepository
 	ProjectUsage() ProjectUsageRepository
 	Onboarding() ProjectOnboardingRepository
+	CredentialsExchangeToken() CredentialsExchangeTokenRepository
 }

+ 45 - 0
internal/repository/test/cred_exchange_token.go

@@ -0,0 +1,45 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+type CredentialsExchangeTokenRepository struct {
+	canQuery bool
+	ceTokens []*models.CredentialsExchangeToken
+}
+
+func NewCredentialsExchangeTokenRepository(canQuery bool) repository.CredentialsExchangeTokenRepository {
+	return &CredentialsExchangeTokenRepository{canQuery, []*models.CredentialsExchangeToken{}}
+}
+
+func (repo *CredentialsExchangeTokenRepository) CreateCredentialsExchangeToken(
+	a *models.CredentialsExchangeToken,
+) (*models.CredentialsExchangeToken, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.ceTokens = append(repo.ceTokens, a)
+	a.ID = uint(len(repo.ceTokens))
+
+	return a, nil
+}
+
+// ReadPWResetToken gets an auth code object specified by the unique code
+func (repo *CredentialsExchangeTokenRepository) ReadCredentialsExchangeToken(id uint) (*models.CredentialsExchangeToken, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.ceTokens) || repo.ceTokens[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.ceTokens[index], nil
+}

+ 10 - 0
internal/repository/test/repository.go

@@ -32,6 +32,7 @@ type TestRepository struct {
 	event                     repository.EventRepository
 	projectUsage              repository.ProjectUsageRepository
 	onboarding                repository.ProjectOnboardingRepository
+	ceToken                   repository.CredentialsExchangeTokenRepository
 }
 
 func (t *TestRepository) User() repository.UserRepository {
@@ -138,8 +139,13 @@ func (t *TestRepository) ProjectUsage() repository.ProjectUsageRepository {
 	return t.projectUsage
 }
 
+<<<<<<< HEAD
 func (t *TestRepository) Onboarding() repository.ProjectOnboardingRepository {
 	return t.onboarding
+=======
+func (t *TestRepository) CredentialsExchangeToken() repository.CredentialsExchangeTokenRepository {
+	return t.ceToken
+>>>>>>> belanger/por-132-vault-storage-backend
 }
 
 // NewRepository returns a Repository which persists users in memory
@@ -172,6 +178,10 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		notificationConfig:        NewNotificationConfigRepository(canQuery),
 		event:                     NewEventRepository(canQuery),
 		projectUsage:              NewProjectUsageRepository(canQuery),
+<<<<<<< HEAD
 		onboarding:                NewProjectOnboardingRepository(canQuery),
+=======
+		ceToken:                   NewCredentialsExchangeTokenRepository(canQuery),
+>>>>>>> belanger/por-132-vault-storage-backend
 	}
 }

+ 14 - 0
scripts/dev-environment/RunMigrateDev.sh

@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# Load env variables for backend
+if [[ -e ./docker/.env ]]
+then
+  set -a # automatically export all variables
+  source ./docker/.env
+  set +a
+else 
+  echo "Couldn't find any backend env variables, exiting process"
+  exit
+fi
+
+go run -tags ee ./cmd/migrate

+ 15 - 1
services/usage/usage.go

@@ -1,3 +1,5 @@
+// +build ee
+
 package usage
 
 import (
@@ -6,6 +8,7 @@ import (
 
 	"github.com/porter-dev/porter/api/server/shared/config/env"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/ee/integrations/vault"
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
@@ -14,6 +17,7 @@ import (
 	"golang.org/x/oauth2"
 	"gorm.io/gorm"
 
+	"github.com/porter-dev/porter/internal/repository/credentials"
 	rgorm "github.com/porter-dev/porter/internal/repository/gorm"
 )
 
@@ -42,13 +46,23 @@ func NewUsageTracker(opts *UsageTrackerOpts) (*UsageTracker, error) {
 		return nil, err
 	}
 
+	var credBackend credentials.CredentialStorage
+
+	if opts.DBConf.VaultAPIKey != "" && opts.DBConf.VaultServerURL != "" && opts.DBConf.VaultPrefix != "" {
+		credBackend = vault.NewClient(
+			opts.DBConf.VaultServerURL,
+			opts.DBConf.VaultAPIKey,
+			opts.DBConf.VaultPrefix,
+		)
+	}
+
 	var key [32]byte
 
 	for i, b := range []byte(opts.DBConf.EncryptionKey) {
 		key[i] = b
 	}
 
-	repo := rgorm.NewRepository(db, &key)
+	repo := rgorm.NewRepository(db, &key, credBackend)
 
 	doConf := oauth.NewDigitalOceanClient(&oauth.Config{
 		ClientID:     opts.DOClientID,