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

Merge pull request #224 from porter-dev/beta.3.do-provisioning-integration

DigitalOcean Integration (container registry and kubernetes)
abelanger5 5 лет назад
Родитель
Сommit
a75c36922c
47 измененных файлов с 2661 добавлено и 100 удалено
  1. 32 0
      cli/cmd/api/integration.go
  2. 87 0
      cli/cmd/api/registry.go
  3. 26 0
      cli/cmd/connect.go
  4. 114 0
      cli/cmd/connect/docr.go
  5. 43 12
      cli/cmd/docker.go
  6. 1 1
      cli/cmd/server.go
  7. 1 1
      cli/cmd/version.go
  8. 69 1
      cmd/docker-credential-porter/helper/helper.go
  9. 3 0
      go.mod
  10. 4 0
      go.sum
  11. 2 0
      internal/config/config.go
  12. 52 0
      internal/forms/infra.go
  13. 2 0
      internal/forms/registry.go
  14. 65 14
      internal/helm/agent.go
  15. 37 26
      internal/helm/config.go
  16. 443 0
      internal/helm/postrenderer.go
  17. 187 0
      internal/kubernetes/agent.go
  18. 27 1
      internal/kubernetes/config.go
  19. 20 0
      internal/kubernetes/provisioner/do/do.go
  20. 25 0
      internal/kubernetes/provisioner/do/docr/docr.go
  21. 25 0
      internal/kubernetes/provisioner/do/doks/doks.go
  22. 53 0
      internal/kubernetes/provisioner/global_stream.go
  23. 18 0
      internal/kubernetes/provisioner/provisioner.go
  24. 2 0
      internal/models/cluster.go
  25. 10 4
      internal/models/infra.go
  26. 40 1
      internal/models/integrations/gcp.go
  27. 1 0
      internal/models/integrations/integration.go
  28. 5 2
      internal/models/integrations/oauth.go
  29. 3 0
      internal/models/registry.go
  30. 50 0
      internal/oauth/config.go
  31. 275 2
      internal/registry/registry.go
  32. 23 0
      internal/repository/gorm/auth.go
  33. 12 0
      internal/repository/gorm/cluster.go
  34. 1 0
      internal/repository/integrations.go
  35. 18 0
      internal/repository/memory/auth.go
  36. 7 2
      internal/templater/helm/values/writer.go
  37. 10 0
      server/api/api.go
  38. 25 6
      server/api/deploy_handler.go
  39. 31 0
      server/api/integration_handler.go
  40. 10 5
      server/api/k8s_handler.go
  41. 99 0
      server/api/oauth_do_handler.go
  42. 1 1
      server/api/oauth_github_handler.go
  43. 356 0
      server/api/provision_handler.go
  44. 62 2
      server/api/registry_handler.go
  45. 76 14
      server/api/release_handler.go
  46. 104 0
      server/router/middleware/auth.go
  47. 104 5
      server/router/router.go

+ 32 - 0
cli/cmd/api/integration.go

@@ -147,3 +147,35 @@ func (c *Client) CreateBasicAuthIntegration(
 
 	return bodyResp, nil
 }
+
+// ListOAuthIntegrationResponse is the list of oauth integrations in a project
+type ListOAuthIntegrationResponse []ints.OAuthIntegrationExternal
+
+// ListOAuthIntegrations lists the oauth integrations in a project
+func (c *Client) ListOAuthIntegrations(
+	ctx context.Context,
+	projectID uint,
+) (ListOAuthIntegrationResponse, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/integrations/oauth", c.BaseURL, projectID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &ListOAuthIntegrationResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return *bodyResp, nil
+}

+ 87 - 0
cli/cmd/api/registry.go

@@ -106,6 +106,53 @@ func (c *Client) CreateGCR(
 	return bodyResp, nil
 }
 
+// CreateDOCRRequest represents the accepted fields for creating
+// a DOCR registry
+type CreateDOCRRequest struct {
+	Name            string `json:"name"`
+	DOIntegrationID uint   `json:"do_integration_id"`
+	URL             string `json:"url"`
+}
+
+// CreateDOCRResponse is the resulting registry after creation
+type CreateDOCRResponse models.RegistryExternal
+
+// CreateDOCR creates an Digital Ocean Container Registry integration
+func (c *Client) CreateDOCR(
+	ctx context.Context,
+	projectID uint,
+	createDOCR *CreateDOCRRequest,
+) (*CreateDOCRResponse, error) {
+	data, err := json.Marshal(createDOCR)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest(
+		"POST",
+		fmt.Sprintf("%s/projects/%d/registries", c.BaseURL, projectID),
+		strings.NewReader(string(data)),
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &CreateDOCRResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}
+
 // ListRegistryResponse is the list of registries for a project
 type ListRegistryResponse []models.RegistryExternal
 
@@ -243,6 +290,46 @@ func (c *Client) GetGCRAuthorizationToken(
 	return bodyResp, nil
 }
 
+type GetDOCRTokenRequest struct {
+	ServerURL string `json:"server_url"`
+}
+
+// GetDOCRAuthorizationToken gets a DOCR authorization token
+func (c *Client) GetDOCRAuthorizationToken(
+	ctx context.Context,
+	projectID uint,
+	docrRequest *GetDOCRTokenRequest,
+) (*GetTokenResponse, error) {
+	data, err := json.Marshal(docrRequest)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/registries/docr/token", c.BaseURL, projectID),
+		strings.NewReader(string(data)),
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	bodyResp := &GetTokenResponse{}
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}
+
 // ListRegistryRepositoryResponse is the list of repositories in a registry
 type ListRegistryRepositoryResponse []registry.Repository
 

+ 26 - 0
cli/cmd/connect.go

@@ -55,6 +55,18 @@ var connectGCRCmd = &cobra.Command{
 	},
 }
 
+var connectDOCRCmd = &cobra.Command{
+	Use:   "docr",
+	Short: "Adds a DOCR instance to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, runConnectDOCR)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 var connectHRCmd = &cobra.Command{
 	Use:     "helmrepo",
 	Aliases: []string{"helm", "helmrepos"},
@@ -103,6 +115,7 @@ func init() {
 
 	connectCmd.AddCommand(connectECRCmd)
 	connectCmd.AddCommand(connectGCRCmd)
+	connectCmd.AddCommand(connectDOCRCmd)
 	connectCmd.AddCommand(connectHRCmd)
 }
 
@@ -154,6 +167,19 @@ func runConnectGCR(_ *api.AuthCheckResponse, client *api.Client, _ []string) err
 	return setRegistry(regID)
 }
 
+func runConnectDOCR(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
+	regID, err := connect.DOCR(
+		client,
+		getProjectID(),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return setRegistry(regID)
+}
+
 func runConnectHelmRepoBasic(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
 	hrID, err := connect.Helm(
 		client,

+ 114 - 0
cli/cmd/connect/docr.go

@@ -0,0 +1,114 @@
+package connect
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// DOCR creates a DOCR integration
+func DOCR(
+	client *api.Client,
+	projectID uint,
+) (uint, error) {
+	// if project ID is 0, ask the user to set the project ID or create a project
+	if projectID == 0 {
+		return 0, fmt.Errorf("no project set, please run porter project set [id]")
+	}
+
+	// list oauth integrations and make sure DO exists
+	oauthInts, err := client.ListOAuthIntegrations(context.TODO(), projectID)
+
+	if err != nil {
+		return 0, err
+	}
+
+	linkedDO := false
+	var doAuth ints.OAuthIntegrationExternal
+
+	// iterate through oauth integrations to find do
+	for _, oauthInt := range oauthInts {
+		if oauthInt.Client == ints.OAuthDigitalOcean {
+			linkedDO = true
+			doAuth = oauthInt
+			break
+		}
+	}
+
+	if !linkedDO {
+		doAuth, err = triggerDigitalOceanOAuth(client, projectID)
+
+		if err != nil {
+			return 0, err
+		}
+	}
+
+	// use the digital ocean oauth to create a registry
+	regURL, err := utils.PromptPlaintext(fmt.Sprintf(`Please provide the registry URL, in the form registry.digitalocean.com/[REGISTRY_NAME]. For example, registry.digitalocean.com/porter-test. 
+Registry URL: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	urlArr := strings.Split(regURL, "/")
+	regName := urlArr[len(urlArr)-1]
+
+	if err != nil {
+		return 0, err
+	}
+
+	reg, err := client.CreateDOCR(
+		context.Background(),
+		projectID,
+		&api.CreateDOCRRequest{
+			Name:            regName,
+			DOIntegrationID: doAuth.ID,
+			URL:             regURL,
+		},
+	)
+
+	return reg.ID, nil
+}
+
+func triggerDigitalOceanOAuth(client *api.Client, projectID uint) (ints.OAuthIntegrationExternal, error) {
+	var doAuth ints.OAuthIntegrationExternal
+
+	oauthURL := fmt.Sprintf("%s/oauth/projects/%d/digitalocean", client.BaseURL, projectID)
+
+	fmt.Printf("Please visit %s in your browser to connect to Digital Ocean (it should open automatically).", oauthURL)
+	utils.OpenBrowser(oauthURL)
+
+	for {
+		oauthInts, err := client.ListOAuthIntegrations(context.TODO(), projectID)
+
+		if err != nil {
+			return doAuth, err
+		}
+
+		linkedDO := false
+
+		// iterate through oauth integrations to find do
+		for _, oauthInt := range oauthInts {
+			if oauthInt.Client == ints.OAuthDigitalOcean {
+				linkedDO = true
+				doAuth = oauthInt
+				break
+			}
+		}
+
+		if linkedDO {
+			break
+		}
+
+		time.Sleep(2 * time.Second)
+	}
+
+	return doAuth, nil
+}

+ 43 - 12
cli/cmd/docker.go

@@ -6,9 +6,11 @@ import (
 	"io/ioutil"
 	"net/url"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"strings"
 
+	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
 	"github.com/porter-dev/porter/cli/cmd/github"
 	"github.com/spf13/cobra"
@@ -93,21 +95,30 @@ func dockerConfig(user *api.AuthCheckResponse, client *api.Client, args []string
 		return err
 	}
 
-	// download the porter cred helper
-	z := &github.ZIPReleaseGetter{
-		AssetName:           "docker-credential-porter",
-		AssetFolderDest:     "/usr/local/bin",
-		ZipFolderDest:       filepath.Join(home, ".porter"),
-		ZipName:             "docker-credential-porter_latest.zip",
-		EntityID:            "porter-dev",
-		RepoName:            "porter",
-		IsPlatformDependent: true,
+	// check if the docker credential helper exists
+	if !commandExists("docker-credential-porter") {
+		err := downloadCredMatchingRelease()
+
+		if err != nil {
+			color.New(color.FgRed).Println("Failed to download credential helper binary:", err.Error())
+			os.Exit(1)
+		}
 	}
 
-	err = z.GetLatestRelease()
+	// otherwise, check the version flag of the binary
+	cmdVersionCred := exec.Command("docker-credential-porter", "--version")
+	writer := &versionWriter{}
+	cmdVersionCred.Stdout = writer
 
-	if err != nil {
-		return err
+	err = cmdVersionCred.Run()
+
+	if err != nil || writer.Version != Version {
+		err := downloadCredMatchingRelease()
+
+		if err != nil {
+			color.New(color.FgRed).Println("Failed to download credential helper binary:", err.Error())
+			os.Exit(1)
+		}
 	}
 
 	config := &configfile.ConfigFile{
@@ -126,3 +137,23 @@ func dockerConfig(user *api.AuthCheckResponse, client *api.Client, args []string
 
 	return config.Save()
 }
+
+func downloadCredMatchingRelease() error {
+	// download the porter cred helper
+	z := &github.ZIPReleaseGetter{
+		AssetName:           "docker-credential-porter",
+		AssetFolderDest:     "/usr/local/bin",
+		ZipFolderDest:       filepath.Join(home, ".porter"),
+		ZipName:             "docker-credential-porter_latest.zip",
+		EntityID:            "porter-dev",
+		RepoName:            "porter",
+		IsPlatformDependent: true,
+	}
+
+	return z.GetRelease(Version)
+}
+
+func commandExists(cmd string) bool {
+	_, err := exec.LookPath(cmd)
+	return err == nil
+}

+ 1 - 1
cli/cmd/server.go

@@ -270,7 +270,7 @@ func downloadMatchingRelease(porterDir string) error {
 		IsPlatformDependent: false,
 	}
 
-	return zStatic.GetLatestRelease()
+	return zStatic.GetRelease(Version)
 }
 
 type versionWriter struct {

+ 1 - 1
cli/cmd/version.go

@@ -7,7 +7,7 @@ import (
 )
 
 // Version will be linked by an ldflag during build
-var Version string = "v0.1.0-beta.3.1"
+var Version string = "dev"
 
 var versionCmd = &cobra.Command{
 	Use:     "version",

+ 69 - 1
cmd/docker-credential-porter/helper/helper.go

@@ -48,13 +48,15 @@ func (p *PorterHelper) Get(serverURL string) (user string, secret string, err er
 
 	if strings.Contains(serverURL, "gcr.io") {
 		return p.getGCR(serverURL)
+	} else if strings.Contains(serverURL, "registry.digitalocean.com") {
+		return p.getDOCR(serverURL)
 	}
 
 	return p.getECR(serverURL)
 }
 
 func (p *PorterHelper) getGCR(serverURL string) (user string, secret string, err error) {
-	urlP, err := url.Parse(serverURL)
+	urlP, err := url.Parse("https://" + serverURL)
 
 	if err != nil {
 		return "", "", err
@@ -96,6 +98,72 @@ func (p *PorterHelper) getGCR(serverURL string) (user string, secret string, err
 	return "oauth2accesstoken", token, nil
 }
 
+func (p *PorterHelper) getDOCR(serverURL string) (user string, secret string, err error) {
+	urlP, err := url.Parse("https://" + serverURL)
+
+	if err != nil {
+		if p.Debug {
+			log.Printf("Error: %s\n", err.Error())
+		}
+
+		return "", "", err
+	}
+
+	credCache := BuildCredentialsCache(urlP.Host)
+	cachedEntry := credCache.Get(serverURL)
+
+	var token string
+
+	if p.Debug {
+		log.Printf("GETTING FROM DOCR", urlP)
+	}
+
+	if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
+		token = cachedEntry.AuthorizationToken
+
+		if p.Debug {
+			log.Printf("USING CACHED TOKEN", token)
+		}
+	} else {
+		host := viper.GetString("host")
+		projID := viper.GetUint("project")
+
+		client := api.NewClient(host+"/api", "cookie.json")
+
+		if p.Debug {
+			log.Printf("MAKING REQUEST", host, projID)
+		}
+
+		// get a token from the server
+		tokenResp, err := client.GetDOCRAuthorizationToken(context.Background(), projID, &api.GetDOCRTokenRequest{
+			ServerURL: serverURL,
+		})
+
+		if err != nil {
+			if p.Debug {
+				log.Printf("Error: %s\n", err.Error())
+			}
+
+			return "", "", err
+		}
+
+		token = tokenResp.Token
+
+		if t := *tokenResp.ExpiresAt; len(token) > 0 && !t.IsZero() {
+			// set the token in cache
+			credCache.Set(serverURL, &AuthEntry{
+				AuthorizationToken: token,
+				RequestedAt:        time.Now(),
+				ExpiresAt:          t,
+				ProxyEndpoint:      serverURL,
+			})
+		}
+
+	}
+
+	return token, token, nil
+}
+
 func (p *PorterHelper) getECR(serverURL string) (user string, secret string, err error) {
 	// parse the server url for region
 	matches := ecrPattern.FindStringSubmatch(serverURL)

+ 3 - 0
go.mod

@@ -13,7 +13,9 @@ require (
 	github.com/containerd/containerd v1.4.1 // indirect
 	github.com/coreos/rkt v1.30.0
 	github.com/creack/pty v1.1.11 // indirect
+	github.com/digitalocean/godo v1.56.0
 	github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492
+	github.com/docker/distribution v2.7.1+incompatible
 	github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce
 	github.com/docker/docker-credential-helpers v0.6.3
 	github.com/docker/go-connections v0.4.0
@@ -27,6 +29,7 @@ require (
 	github.com/go-redis/redis/v7 v7.4.0
 	github.com/go-redis/redis/v8 v8.3.1
 	github.com/go-test/deep v1.0.7
+	github.com/google/go-cmp v0.5.2
 	github.com/google/go-github v17.0.0+incompatible
 	github.com/google/go-querystring v1.0.0 // indirect
 	github.com/googleapis/gnostic v0.2.2 // indirect

+ 4 - 0
go.sum

@@ -230,6 +230,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/digitalocean/godo v1.56.0 h1:wXqWJyywrDO3YO2T4Kh8TwbCPOa+OI2vC8qh0/Ngmjk=
+github.com/digitalocean/godo v1.56.0/go.mod h1:p7dOjjtSBqCTUksqtA5Fd3uaKs9kyTq2xcz76ulEJRU=
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
 github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492 h1:FwssHbCDJD025h+BchanCwE1Q8fyMgqDr2mOQAWOLGw=
 github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
@@ -1476,6 +1478,8 @@ k8s.io/cli-runtime v0.18.8/go.mod h1:7EzWiDbS9PFd0hamHHVoCY4GrokSTPSL32MA4rzIu0M
 k8s.io/client-go v0.16.8/go.mod h1:WmPuN0yJTKHXoklExKxzo3jSXmr3EnN+65uaTb5VuNs=
 k8s.io/client-go v0.18.8 h1:SdbLpIxk5j5YbFr1b7fq8S7mDgDjYmUxSbszyoesoDM=
 k8s.io/client-go v0.18.8/go.mod h1:HqFqMllQ5NnQJNwjro9k5zMyfhZlOwpuTLVrxjkYSxU=
+k8s.io/client-go v1.5.1 h1:XaX/lo2/u3/pmFau8HN+sB5C/b4dc4Dmm2eXjBH4p1E=
+k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o=
 k8s.io/code-generator v0.16.8/go.mod h1:wFdrXdVi/UC+xIfLi+4l9elsTT/uEF61IfcN2wOLULQ=
 k8s.io/code-generator v0.18.8/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
 k8s.io/component-base v0.16.8/go.mod h1:Q8UWOWShpP3MZZny4n/15gOncfaaVtc9SbCdkM5MhUE=

+ 2 - 0
internal/config/config.go

@@ -34,6 +34,8 @@ type ServerConf struct {
 	GithubClientID     string `env:"GITHUB_CLIENT_ID"`
 	GithubClientSecret string `env:"GITHUB_CLIENT_SECRET"`
 
+	DOClientID          string `env:"DO_CLIENT_ID"`
+	DOClientSecret      string `env:"DO_CLIENT_SECRET"`
 	ProvisionerImageTag string `env:"PROV_IMAGE_TAG,default-latest"`
 }
 

+ 52 - 0
internal/forms/infra.go

@@ -84,6 +84,46 @@ func (ce *CreateGKEInfra) ToInfra() (*models.Infra, error) {
 	}, nil
 }
 
+// CreateDOCRInfra represents the accepted values for creating an
+// DOCR infra via the provisioning container
+type CreateDOCRInfra struct {
+	DOCRName             string `json:"docr_name" form:"required"`
+	DOCRSubscriptionTier string `json:"docr_subscription_tier" form:"required"`
+	ProjectID            uint   `json:"project_id" form:"required"`
+	DOIntegrationID      uint   `json:"do_integration_id" form:"required"`
+}
+
+// ToInfra converts the form to a gorm infra model
+func (de *CreateDOCRInfra) ToInfra() (*models.Infra, error) {
+	return &models.Infra{
+		Kind:            models.InfraDOCR,
+		ProjectID:       de.ProjectID,
+		Suffix:          stringWithCharset(6, randCharset),
+		Status:          models.StatusCreating,
+		DOIntegrationID: de.DOIntegrationID,
+	}, nil
+}
+
+// CreateDOKSInfra represents the accepted values for creating a
+// DOKS infra via the provisioning container
+type CreateDOKSInfra struct {
+	DORegion        string `json:"do_region" form:"required"`
+	DOKSName        string `json:"doks_name" form:"required"`
+	ProjectID       uint   `json:"project_id" form:"required"`
+	DOIntegrationID uint   `json:"do_integration_id" form:"required"`
+}
+
+// ToInfra converts the form to a gorm infra model
+func (de *CreateDOKSInfra) ToInfra() (*models.Infra, error) {
+	return &models.Infra{
+		Kind:            models.InfraDOKS,
+		ProjectID:       de.ProjectID,
+		Suffix:          stringWithCharset(6, randCharset),
+		Status:          models.StatusCreating,
+		DOIntegrationID: de.DOIntegrationID,
+	}, nil
+}
+
 // DestroyECRInfra represents the accepted values for destroying an
 // ECR infra via the provisioning container
 type DestroyECRInfra struct {
@@ -102,6 +142,18 @@ type DestroyGKEInfra struct {
 	GKEName string `json:"gke_name" form:"required"`
 }
 
+// DestroyDOCRInfra represents the accepted values for destroying an
+// DOCR infra via the provisioning container
+type DestroyDOCRInfra struct {
+	DOCRName string `json:"docr_name" form:"required"`
+}
+
+// DestroyDOKSInfra represents the accepted values for destroying an
+// DOKS infra via the provisioning container
+type DestroyDOKSInfra struct {
+	DOKSName string `json:"doks_name" form:"required"`
+}
+
 // helpers for random string
 var seededRand *rand.Rand = rand.New(
 	rand.NewSource(time.Now().UnixNano()))

+ 2 - 0
internal/forms/registry.go

@@ -14,6 +14,7 @@ type CreateRegistry struct {
 	URL              string `json:"url"`
 	GCPIntegrationID uint   `json:"gcp_integration_id"`
 	AWSIntegrationID uint   `json:"aws_integration_id"`
+	DOIntegrationID  uint   `json:"do_integration_id"`
 }
 
 // ToRegistry converts the form to a gorm registry model
@@ -24,6 +25,7 @@ func (cr *CreateRegistry) ToRegistry(repo repository.Repository) (*models.Regist
 		URL:              cr.URL,
 		GCPIntegrationID: cr.GCPIntegrationID,
 		AWSIntegrationID: cr.AWSIntegrationID,
+		DOIntegrationID:  cr.DOIntegrationID,
 	}
 
 	if registry.URL == "" && registry.AWSIntegrationID != 0 {

+ 65 - 14
internal/helm/agent.go

@@ -4,6 +4,10 @@ import (
 	"fmt"
 
 	"github.com/pkg/errors"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/oauth2"
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/release"
@@ -13,6 +17,7 @@ import (
 // Agent is a Helm agent for performing helm operations
 type Agent struct {
 	ActionConfig *action.Configuration
+	K8sAgent     *kubernetes.Agent
 }
 
 // ListReleases lists releases based on a ListFilter
@@ -49,10 +54,19 @@ func (a *Agent) GetReleaseHistory(
 	return cmd.Run(name)
 }
 
+type UpgradeReleaseConfig struct {
+	Name       string
+	Values     map[string]interface{}
+	Cluster    *models.Cluster
+	Repo       repository.Repository
+	Registries []*models.Registry
+}
+
 // UpgradeRelease upgrades a specific release with new values.yaml
 func (a *Agent) UpgradeRelease(
-	name string,
+	conf *UpgradeReleaseConfig,
 	values string,
+	doAuth *oauth2.Config,
 ) (*release.Release, error) {
 	valuesYaml, err := chartutil.ReadValues([]byte(values))
 
@@ -60,16 +74,18 @@ func (a *Agent) UpgradeRelease(
 		return nil, fmt.Errorf("Values could not be parsed: %v", err)
 	}
 
-	return a.UpgradeReleaseByValues(name, valuesYaml)
+	conf.Values = valuesYaml
+
+	return a.UpgradeReleaseByValues(conf, doAuth)
 }
 
 // UpgradeReleaseByValues upgrades a release by unmarshaled yaml values
 func (a *Agent) UpgradeReleaseByValues(
-	name string,
-	values map[string]interface{},
+	conf *UpgradeReleaseConfig,
+	doAuth *oauth2.Config,
 ) (*release.Release, error) {
 	// grab the latest release
-	rel, err := a.GetRelease(name, 0)
+	rel, err := a.GetRelease(conf.Name, 0)
 
 	if err != nil {
 		return nil, fmt.Errorf("Could not get release to be upgraded: %v", err)
@@ -78,7 +94,23 @@ func (a *Agent) UpgradeReleaseByValues(
 	ch := rel.Chart
 
 	cmd := action.NewUpgrade(a.ActionConfig)
-	res, err := cmd.Run(name, ch, values)
+
+	if conf.Cluster != nil && a.K8sAgent != nil && conf.Registries != nil && len(conf.Registries) > 0 {
+		cmd.PostRenderer, err = NewDockerSecretsPostRenderer(
+			conf.Cluster,
+			conf.Repo,
+			a.K8sAgent,
+			rel.Namespace,
+			conf.Registries,
+			doAuth,
+		)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	res, err := cmd.Run(conf.Name, ch, conf.Values)
 
 	if err != nil {
 		return nil, fmt.Errorf("Upgrade failed: %v", err)
@@ -89,16 +121,20 @@ func (a *Agent) UpgradeReleaseByValues(
 
 // InstallChartConfig is the config required to install a chart
 type InstallChartConfig struct {
-	Chart     *chart.Chart
-	Name      string
-	Namespace string
-	Values    map[string]interface{}
+	Chart      *chart.Chart
+	Name       string
+	Namespace  string
+	Values     map[string]interface{}
+	Cluster    *models.Cluster
+	Repo       repository.Repository
+	Registries []*models.Registry
 }
 
 // InstallChartFromValuesBytes reads the raw values and calls Agent.InstallChart
 func (a *Agent) InstallChartFromValuesBytes(
 	conf *InstallChartConfig,
 	values []byte,
+	doAuth *oauth2.Config,
 ) (*release.Release, error) {
 	valuesYaml, err := chartutil.ReadValues(values)
 
@@ -108,12 +144,13 @@ func (a *Agent) InstallChartFromValuesBytes(
 
 	conf.Values = valuesYaml
 
-	return a.InstallChart(conf)
+	return a.InstallChart(conf, doAuth)
 }
 
 // InstallChart installs a new chart
 func (a *Agent) InstallChart(
 	conf *InstallChartConfig,
+	doAuth *oauth2.Config,
 ) (*release.Release, error) {
 	cmd := action.NewInstall(a.ActionConfig)
 
@@ -128,9 +165,23 @@ func (a *Agent) InstallChart(
 		return nil, err
 	}
 
-	// if chartRequested.Metadata.Deprecated {
-	// 	return nil, fmt.Errorf("This chart is deprecated")
-	// }
+	var err error
+
+	// only add the postrenderer if required fields exist
+	if conf.Cluster != nil && a.K8sAgent != nil && conf.Registries != nil && len(conf.Registries) > 0 {
+		cmd.PostRenderer, err = NewDockerSecretsPostRenderer(
+			conf.Cluster,
+			conf.Repo,
+			a.K8sAgent,
+			conf.Namespace,
+			conf.Registries,
+			doAuth,
+		)
+
+		if err != nil {
+			return nil, err
+		}
+	}
 
 	if req := conf.Chart.Metadata.Dependencies; req != nil {
 		if err := action.CheckDependencies(conf.Chart, req); err != nil {

+ 37 - 26
internal/helm/config.go

@@ -8,6 +8,7 @@ import (
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/oauth2"
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/chartutil"
 	"helm.sh/helm/v3/pkg/kube"
@@ -19,10 +20,11 @@ import (
 // Form represents the options for connecting to a cluster and
 // creating a Helm agent
 type Form struct {
-	Cluster   *models.Cluster `form:"required"`
-	Repo      *repository.Repository
-	Storage   string `json:"storage" form:"oneof=secret configmap memory" default:"secret"`
-	Namespace string `json:"namespace"`
+	Cluster           *models.Cluster `form:"required"`
+	Repo              *repository.Repository
+	DigitalOceanOAuth *oauth2.Config
+	Storage           string `json:"storage" form:"oneof=secret configmap memory" default:"secret"`
+	Namespace         string `json:"namespace"`
 }
 
 // GetAgentOutOfClusterConfig creates a new Agent from outside the cluster using
@@ -30,8 +32,9 @@ type Form struct {
 func GetAgentOutOfClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
 	// create a kubernetes agent
 	conf := &kubernetes.OutOfClusterConfig{
-		Cluster: form.Cluster,
-		Repo:    form.Repo,
+		Cluster:           form.Cluster,
+		Repo:              form.Repo,
+		DigitalOceanOAuth: form.DigitalOceanOAuth,
 	}
 
 	k8sAgent, err := kubernetes.GetAgentOutOfClusterConfig(conf)
@@ -52,12 +55,15 @@ func GetAgentFromK8sAgent(stg string, ns string, l *logger.Logger, k8sAgent *kub
 	}
 
 	// use k8s agent to create Helm agent
-	return &Agent{&action.Configuration{
-		RESTClientGetter: k8sAgent.RESTClientGetter,
-		KubeClient:       kube.New(k8sAgent.RESTClientGetter),
-		Releases:         StorageMap[stg](l, clientset.CoreV1(), ns),
-		Log:              l.Printf,
-	}}, nil
+	return &Agent{
+		ActionConfig: &action.Configuration{
+			RESTClientGetter: k8sAgent.RESTClientGetter,
+			KubeClient:       kube.New(k8sAgent.RESTClientGetter),
+			Releases:         StorageMap[stg](l, clientset.CoreV1(), ns),
+			Log:              l.Printf,
+		},
+		K8sAgent: k8sAgent,
+	}, nil
 }
 
 // GetAgentInClusterConfig creates a new Agent from inside the cluster using
@@ -77,12 +83,15 @@ func GetAgentInClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
 	}
 
 	// use k8s agent to create Helm agent
-	return &Agent{&action.Configuration{
-		RESTClientGetter: k8sAgent.RESTClientGetter,
-		KubeClient:       kube.New(k8sAgent.RESTClientGetter),
-		Releases:         StorageMap[form.Storage](l, clientset.CoreV1(), form.Namespace),
-		Log:              l.Printf,
-	}}, nil
+	return &Agent{
+		ActionConfig: &action.Configuration{
+			RESTClientGetter: k8sAgent.RESTClientGetter,
+			KubeClient:       kube.New(k8sAgent.RESTClientGetter),
+			Releases:         StorageMap[form.Storage](l, clientset.CoreV1(), form.Namespace),
+			Log:              l.Printf,
+		},
+		K8sAgent: k8sAgent,
+	}, nil
 }
 
 // GetAgentTesting creates a new Agent using an optional existing storage class
@@ -93,14 +102,16 @@ func GetAgentTesting(form *Form, storage *storage.Storage, l *logger.Logger) *Ag
 		testStorage = StorageMap["memory"](nil, nil, "")
 	}
 
-	return &Agent{&action.Configuration{
-		Releases: testStorage,
-		KubeClient: &kubefake.FailingKubeClient{
-			PrintingKubeClient: kubefake.PrintingKubeClient{
-				Out: ioutil.Discard,
+	return &Agent{
+		ActionConfig: &action.Configuration{
+			Releases: testStorage,
+			KubeClient: &kubefake.FailingKubeClient{
+				PrintingKubeClient: kubefake.PrintingKubeClient{
+					Out: ioutil.Discard,
+				},
 			},
+			Capabilities: chartutil.DefaultCapabilities,
+			Log:          l.Printf,
 		},
-		Capabilities: chartutil.DefaultCapabilities,
-		Log:          l.Printf,
-	}}
+	}
 }

+ 443 - 0
internal/helm/postrenderer.go

@@ -0,0 +1,443 @@
+package helm
+
+import (
+	"bytes"
+	"io"
+	"net/url"
+	"regexp"
+	"strings"
+
+	"github.com/aws/aws-sdk-go/aws/arn"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/oauth2"
+	"gopkg.in/yaml.v2"
+	"helm.sh/helm/v3/pkg/postrender"
+
+	"github.com/docker/distribution/reference"
+)
+
+// DockerSecretsPostRenderer is a Helm post-renderer that adds image pull secrets to
+// pod specs that would otherwise be unable to pull an image.
+//
+// The post-renderer currently looks for two types of registries: GCR and ECR (TODO: DOCR
+// and Dockerhub). It also detects if the image pull secret is necessary: if GCR image pulls
+// occur in a GKE cluster in the same project, or if ECR image pulls exist in an EKS cluster
+// in the same organization + region, an image pull is not necessary.
+type DockerSecretsPostRenderer struct {
+	Cluster   *models.Cluster
+	Repo      repository.Repository
+	Agent     *kubernetes.Agent
+	Namespace string
+	DOAuth    *oauth2.Config
+
+	registries map[string]*models.Registry
+
+	podSpecs  []resource
+	resources []resource
+}
+
+// while manifests are map[string]interface{} at the top level,
+// nested keys will be of type map[interface{}]interface{}
+type resource map[interface{}]interface{}
+
+func NewDockerSecretsPostRenderer(
+	cluster *models.Cluster,
+	repo repository.Repository,
+	agent *kubernetes.Agent,
+	namespace string,
+	regs []*models.Registry,
+	doAuth *oauth2.Config,
+) (postrender.PostRenderer, error) {
+	// Registries is a map of registry URLs to registry ids
+	registries := make(map[string]*models.Registry)
+
+	for _, reg := range regs {
+		regURL := reg.URL
+
+		if !strings.Contains(regURL, "http") {
+			regURL = "https://" + regURL
+		}
+
+		parsedRegURL, err := url.Parse(regURL)
+
+		if err != nil {
+			continue
+		}
+
+		addReg := parsedRegURL.Host
+
+		if parsedRegURL.Path != "" {
+			addReg += "/" + strings.Trim(parsedRegURL.Path, "/")
+		}
+
+		registries[addReg] = reg
+	}
+
+	return &DockerSecretsPostRenderer{
+		Cluster:    cluster,
+		Repo:       repo,
+		Agent:      agent,
+		Namespace:  namespace,
+		DOAuth:     doAuth,
+		registries: registries,
+		podSpecs:   make([]resource, 0),
+		resources:  make([]resource, 0),
+	}, nil
+}
+
+func (d *DockerSecretsPostRenderer) Run(
+	renderedManifests *bytes.Buffer,
+) (modifiedManifests *bytes.Buffer, err error) {
+	bufCopy := bytes.NewBuffer(renderedManifests.Bytes())
+
+	linkedRegs, err := d.getRegistriesToLink(bufCopy)
+
+	// if we encountered an error here, we'll render the manifests anyway
+	// without modification
+	if err != nil {
+		return renderedManifests, nil
+	}
+
+	// create the necessary secrets
+	secrets, err := d.Agent.CreateImagePullSecrets(
+		d.Repo,
+		d.Namespace,
+		linkedRegs,
+		d.DOAuth,
+	)
+
+	if err != nil {
+		return renderedManifests, nil
+	}
+
+	d.updatePodSpecs(secrets)
+
+	modifiedManifests = bytes.NewBuffer([]byte{})
+	encoder := yaml.NewEncoder(modifiedManifests)
+	defer encoder.Close()
+
+	for _, resource := range d.resources {
+		err = encoder.Encode(resource)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return modifiedManifests, nil
+}
+
+func (d *DockerSecretsPostRenderer) getRegistriesToLink(renderedManifests *bytes.Buffer) (map[string]*models.Registry, error) {
+	// create a map of registry names to registries: these are the registries
+	// that a secret will be generated for, if it does not exist
+	linkedRegs := make(map[string]*models.Registry)
+
+	err := d.decodeRenderedManifests(renderedManifests)
+
+	if err != nil {
+		return linkedRegs, err
+	}
+
+	// read the pod specs into the post-renderer object
+	d.getPodSpecs(d.resources)
+
+	for _, podSpec := range d.podSpecs {
+		// get all images
+		images := d.getImageList(podSpec)
+
+		// read the image url
+		for _, image := range images {
+			named, err := reference.ParseNormalizedNamed(image)
+
+			if err != nil {
+				continue
+			}
+
+			domain := reference.Domain(named)
+			path := reference.Path(named)
+
+			regName := domain
+
+			if pathArr := strings.Split(path, "/"); len(pathArr) > 1 {
+				regName += "/" + strings.Join(pathArr[:len(pathArr)-1], "/")
+			}
+
+			// check if the integration is native to the cluster/registry combination
+			isNative := d.isRegistryNative(regName)
+
+			if isNative {
+				continue
+			}
+
+			reg, exists := d.registries[regName]
+
+			if !exists {
+				continue
+			}
+
+			// if the registry exists, add it to the map
+			linkedRegs[regName] = reg
+		}
+	}
+
+	return linkedRegs, nil
+}
+
+func (d *DockerSecretsPostRenderer) decodeRenderedManifests(
+	renderedManifests *bytes.Buffer,
+) error {
+	// use the yaml decoder to parse the multi-document yaml.
+	decoder := yaml.NewDecoder(renderedManifests)
+
+	for {
+		res := make(resource)
+		err := decoder.Decode(&res)
+		if err == io.EOF {
+			break
+		}
+
+		if err != nil {
+			return err
+		}
+
+		d.resources = append(d.resources, res)
+	}
+
+	return nil
+}
+
+func (d *DockerSecretsPostRenderer) getPodSpecs(resources []resource) {
+	for _, res := range resources {
+		kindVal, hasKind := res["kind"]
+		if !hasKind {
+			continue
+		}
+
+		kind, ok := kindVal.(string)
+		if !ok {
+			continue
+		}
+
+		// manifests of list type will have an items field, items should
+		// be recursively parsed
+		if itemsVal, isList := res["items"]; isList {
+			if items, ok := itemsVal.([]interface{}); ok {
+				// convert items to resource
+				resArr := make([]resource, 0)
+				for _, item := range items {
+					if arrVal, ok := item.(resource); ok {
+						resArr = append(resArr, arrVal)
+					}
+				}
+
+				d.getPodSpecs(resArr)
+			}
+
+			continue
+		}
+
+		// otherwise, get the pod spec based on the type of resource
+		podSpec := getPodSpecFromResource(kind, res)
+
+		if podSpec == nil {
+			continue
+		}
+
+		d.podSpecs = append(d.podSpecs, podSpec)
+	}
+
+	return
+}
+
+func (d *DockerSecretsPostRenderer) updatePodSpecs(secrets map[string]string) {
+	for _, podSpec := range d.podSpecs {
+		containersVal, hasContainers := podSpec["containers"]
+
+		if !hasContainers {
+			continue
+		}
+
+		containers, ok := containersVal.([]interface{})
+
+		if !ok {
+			continue
+		}
+
+		var imagePullSecrets []map[string]interface{}
+		existingNames := map[string]bool{}
+		if existingPullSecrets, ok := podSpec["imagePullSecrets"]; ok {
+			imagePullSecrets = existingPullSecrets.([]map[string]interface{})
+			for _, s := range imagePullSecrets {
+				if name, ok := s["name"]; ok {
+					if n, ok := name.(string); ok {
+						existingNames[n] = true
+					}
+				}
+			}
+		}
+
+		for _, container := range containers {
+			_container, ok := container.(resource)
+
+			if !ok {
+				continue
+			}
+
+			image, ok := _container["image"].(string)
+
+			if !ok {
+				continue
+			}
+
+			named, err := reference.ParseNormalizedNamed(image)
+
+			if err != nil {
+				continue
+			}
+
+			domain := reference.Domain(named)
+			path := reference.Path(named)
+
+			regName := domain
+
+			if pathArr := strings.Split(path, "/"); len(pathArr) > 1 {
+				regName += "/" + strings.Join(pathArr[:len(pathArr)-1], "/")
+			}
+
+			imagePullSecrets = append(imagePullSecrets, map[string]interface{}{
+				"name": secrets[regName],
+			})
+		}
+
+		if len(imagePullSecrets) > 0 {
+			podSpec["imagePullSecrets"] = imagePullSecrets
+		}
+
+	}
+}
+
+func (d *DockerSecretsPostRenderer) getImageList(podSpec resource) []string {
+	images := make([]string, 0)
+
+	containersVal, hasContainers := podSpec["containers"]
+
+	if !hasContainers {
+		return images
+	}
+
+	containers, ok := containersVal.([]interface{})
+
+	if !ok {
+		return images
+	}
+
+	for _, container := range containers {
+		_container, ok := container.(resource)
+
+		if !ok {
+			continue
+		}
+
+		image, ok := _container["image"].(string)
+
+		if !ok {
+			continue
+		}
+
+		images = append(images, image)
+	}
+
+	return images
+}
+
+var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?`)
+
+func (d *DockerSecretsPostRenderer) isRegistryNative(regName string) bool {
+	isNative := false
+
+	if strings.Contains(regName, "gcr") && d.Cluster.AuthMechanism == models.GCP {
+		// get the project id of the cluster
+		gcpInt, err := d.Repo.GCPIntegration.ReadGCPIntegration(d.Cluster.GCPIntegrationID)
+
+		if err != nil {
+			return false
+		}
+
+		gkeProjectID, err := integrations.GCPProjectIDFromJSON(gcpInt.GCPKeyData)
+
+		if err != nil {
+			return false
+		}
+
+		// parse the project id of the gcr url
+		if regNameArr := strings.Split(regName, "/"); len(regNameArr) >= 2 {
+			gcrProjectID := regNameArr[1]
+
+			isNative = gcrProjectID == gkeProjectID
+		}
+	} else if strings.Contains(regName, "ecr") && d.Cluster.AuthMechanism == models.AWS {
+		matches := ecrPattern.FindStringSubmatch(regName)
+
+		if len(matches) < 3 {
+			return false
+		}
+
+		eksAccountID := matches[1]
+		eksRegion := matches[3]
+
+		awsInt, err := d.Repo.AWSIntegration.ReadAWSIntegration(d.Cluster.AWSIntegrationID)
+
+		if err != nil {
+			return false
+		}
+
+		err = awsInt.PopulateAWSArn()
+
+		if err != nil {
+			return false
+		}
+
+		parsedARN, err := arn.Parse(awsInt.AWSArn)
+
+		if err != nil {
+			return false
+		}
+
+		isNative = parsedARN.AccountID == eksAccountID && parsedARN.Region == eksRegion
+	}
+
+	return isNative
+}
+
+func getPodSpecFromResource(kind string, res resource) resource {
+	switch kind {
+	case "Pod":
+		return getNestedResource(res, "spec")
+	case "DaemonSet", "Deployment", "Job", "ReplicaSet", "ReplicationController", "StatefulSet":
+		return getNestedResource(res, "spec", "template", "spec")
+	case "PodTemplate":
+		return getNestedResource(res, "template", "spec")
+	case "CronJob":
+		return getNestedResource(res, "spec", "jobTemplate", "spec", "template", "spec")
+	}
+
+	return nil
+}
+
+func getNestedResource(res resource, keys ...string) resource {
+	curr := res
+	var ok bool
+
+	for _, key := range keys {
+		curr, ok = curr[key].(resource)
+
+		if !ok {
+			return nil
+		}
+	}
+
+	return curr
+}

+ 187 - 0
internal/kubernetes/agent.go

@@ -2,6 +2,7 @@ package kubernetes
 
 import (
 	"bufio"
+	"bytes"
 	"context"
 	"fmt"
 	"io"
@@ -11,10 +12,17 @@ import (
 	"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"
 
 	"github.com/gorilla/websocket"
 	"github.com/porter-dev/porter/internal/helm/grapher"
@@ -22,6 +30,7 @@ import (
 	batchv1 "k8s.io/api/batch/v1"
 	v1 "k8s.io/api/core/v1"
 	v1beta1 "k8s.io/api/extensions/v1beta1"
+	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/client-go/informers"
@@ -364,6 +373,100 @@ func (a *Agent) ProvisionGKE(
 	return a.provision(prov)
 }
 
+// ProvisionDOCR spawns a new provisioning pod that creates a DOCR instance
+func (a *Agent) ProvisionDOCR(
+	projectID uint,
+	doConf *integrations.OAuthIntegration,
+	doAuth *oauth2.Config,
+	repo repository.Repository,
+	docrName, docrSubscriptionTier string,
+	infra *models.Infra,
+	operation provisioner.ProvisionerOperation,
+	pgConf *config.DBConf,
+	redisConf *config.RedisConf,
+) (*batchv1.Job, error) {
+	// get the token
+	oauthInt, err := repo.OAuthIntegration.ReadOAuthIntegration(
+		infra.DOIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	tok, _, err := oauth.GetAccessToken(oauthInt, doAuth, repo)
+
+	if err != nil {
+		return nil, err
+	}
+
+	id := infra.GetID()
+	prov := &provisioner.Conf{
+		ID:        id,
+		Name:      fmt.Sprintf("prov-%s-%s", id, string(operation)),
+		Kind:      provisioner.DOCR,
+		Operation: operation,
+		Redis:     redisConf,
+		Postgres:  pgConf,
+		DO: &do.Conf{
+			DOToken: tok,
+		},
+		DOCR: &docr.Conf{
+			DOCRName:             docrName,
+			DOCRSubscriptionTier: docrSubscriptionTier,
+		},
+	}
+
+	return a.provision(prov)
+}
+
+// ProvisionDOKS spawns a new provisioning pod that creates a DOKS instance
+func (a *Agent) ProvisionDOKS(
+	projectID uint,
+	doConf *integrations.OAuthIntegration,
+	doAuth *oauth2.Config,
+	repo repository.Repository,
+	doRegion, doksClusterName string,
+	infra *models.Infra,
+	operation provisioner.ProvisionerOperation,
+	pgConf *config.DBConf,
+	redisConf *config.RedisConf,
+) (*batchv1.Job, error) {
+	// get the token
+	oauthInt, err := repo.OAuthIntegration.ReadOAuthIntegration(
+		infra.DOIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	tok, _, err := oauth.GetAccessToken(oauthInt, doAuth, repo)
+
+	if err != nil {
+		return nil, err
+	}
+
+	id := infra.GetID()
+	prov := &provisioner.Conf{
+		ID:        id,
+		Name:      fmt.Sprintf("prov-%s-%s", id, string(operation)),
+		Kind:      provisioner.DOKS,
+		Operation: operation,
+		Redis:     redisConf,
+		Postgres:  pgConf,
+		DO: &do.Conf{
+			DOToken: tok,
+		},
+		DOKS: &doks.Conf{
+			DORegion:        doRegion,
+			DOKSClusterName: doksClusterName,
+		},
+	}
+
+	return a.provision(prov)
+}
+
 // ProvisionTest spawns a new provisioning pod that tests provisioning
 func (a *Agent) ProvisionTest(
 	projectID uint,
@@ -402,3 +505,87 @@ func (a *Agent) provision(
 		metav1.CreateOptions{},
 	)
 }
+
+// CreateImagePullSecrets will create the required image pull secrets and
+// return a map from the registry name to the name of the secret.
+func (a *Agent) CreateImagePullSecrets(
+	repo repository.Repository,
+	namespace string,
+	linkedRegs map[string]*models.Registry,
+	doAuth *oauth2.Config,
+) (map[string]string, error) {
+	res := make(map[string]string)
+
+	for key, val := range linkedRegs {
+		_reg := registry.Registry(*val)
+
+		data, err := _reg.GetDockerConfigJSON(repo, doAuth)
+
+		if err != nil {
+			return nil, err
+		}
+
+		secretName := fmt.Sprintf("porter-%s-%d", val.Externalize().Service, val.ID)
+
+		secret, err := a.Clientset.CoreV1().Secrets(namespace).Get(
+			context.TODO(),
+			secretName,
+			metav1.GetOptions{},
+		)
+
+		// if not found, create the secret
+		if err != nil && errors.IsNotFound(err) {
+			_, err = a.Clientset.CoreV1().Secrets(namespace).Create(
+				context.TODO(),
+				&v1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: secretName,
+					},
+					Data: map[string][]byte{
+						string(v1.DockerConfigJsonKey): data,
+					},
+					Type: v1.SecretTypeDockerConfigJson,
+				},
+				metav1.CreateOptions{},
+			)
+
+			if err != nil {
+				return nil, err
+			}
+
+			// add secret name to the map
+			res[key] = secretName
+
+			continue
+		} else if err != nil {
+			return nil, err
+		}
+
+		// otherwise, check that the secret contains the correct data: if
+		// if doesn't, update it
+		if !bytes.Equal(secret.Data[v1.DockerConfigJsonKey], data) {
+			_, err := a.Clientset.CoreV1().Secrets(namespace).Update(
+				context.TODO(),
+				&v1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: secretName,
+					},
+					Data: map[string][]byte{
+						string(v1.DockerConfigJsonKey): data,
+					},
+					Type: v1.SecretTypeDockerConfigJson,
+				},
+				metav1.UpdateOptions{},
+			)
+
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		// add secret name to the map
+		res[key] = secretName
+	}
+
+	return res, nil
+}

+ 27 - 1
internal/kubernetes/config.go

@@ -8,7 +8,9 @@ import (
 	"time"
 
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/oauth2"
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -88,6 +90,9 @@ func GetAgentTesting(objects ...runtime.Object) *Agent {
 type OutOfClusterConfig struct {
 	Cluster *models.Cluster
 	Repo    *repository.Repository
+
+	// Only required if using DigitalOcean OAuth as an auth mechanism
+	DigitalOceanOAuth *oauth2.Config
 }
 
 // ToRESTConfig creates a kubernetes REST client factory -- it calls ClientConfig on
@@ -268,7 +273,11 @@ func (conf *OutOfClusterConfig) createRawConfigFromCluster() (*api.Config, error
 			return nil, err
 		}
 
-		tok, err := gcpAuth.GetBearerToken(conf.getTokenCache, conf.setTokenCache)
+		tok, err := gcpAuth.GetBearerToken(
+			conf.getTokenCache,
+			conf.setTokenCache,
+			"https://www.googleapis.com/auth/cloud-platform",
+		)
 
 		if err != nil {
 			return nil, err
@@ -291,6 +300,23 @@ func (conf *OutOfClusterConfig) createRawConfigFromCluster() (*api.Config, error
 			return nil, err
 		}
 
+		// add this as a bearer token
+		authInfoMap[authInfoName].Token = tok
+	case models.DO:
+		oauthInt, err := conf.Repo.OAuthIntegration.ReadOAuthIntegration(
+			cluster.DOIntegrationID,
+		)
+
+		if err != nil {
+			return nil, err
+		}
+
+		tok, _, err := oauth.GetAccessToken(oauthInt, conf.DigitalOceanOAuth, *conf.Repo)
+
+		if err != nil {
+			return nil, err
+		}
+
 		// add this as a bearer token
 		authInfoMap[authInfoName].Token = tok
 	default:

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

@@ -0,0 +1,20 @@
+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
+}

+ 25 - 0
internal/kubernetes/provisioner/do/docr/docr.go

@@ -0,0 +1,25 @@
+package docr
+
+import (
+	v1 "k8s.io/api/core/v1"
+)
+
+// Conf is just a DO token
+type Conf struct {
+	DOCRName, DOCRSubscriptionTier string
+}
+
+// AttachDOCREnv adds the relevant DO env for the provisioner
+func (conf *Conf) AttachDOCREnv(env []v1.EnvVar) []v1.EnvVar {
+	env = append(env, v1.EnvVar{
+		Name:  "DOCR_NAME",
+		Value: conf.DOCRName,
+	})
+
+	env = append(env, v1.EnvVar{
+		Name:  "DOCR_SUBSCRIPTION_TIER",
+		Value: conf.DOCRSubscriptionTier,
+	})
+
+	return env
+}

+ 25 - 0
internal/kubernetes/provisioner/do/doks/doks.go

@@ -0,0 +1,25 @@
+package doks
+
+import (
+	v1 "k8s.io/api/core/v1"
+)
+
+// Conf is just a DO token
+type Conf struct {
+	DORegion, DOKSClusterName string
+}
+
+// AttachDOKSEnv adds the relevant DO env for the provisioner
+func (conf *Conf) AttachDOKSEnv(env []v1.EnvVar) []v1.EnvVar {
+	env = append(env, v1.EnvVar{
+		Name:  "DO_REGION",
+		Value: conf.DORegion,
+	})
+
+	env = append(env, v1.EnvVar{
+		Name:  "DOKS_CLUSTER_NAME",
+		Value: conf.DOKSClusterName,
+	})
+
+	return env
+}

+ 53 - 0
internal/kubernetes/provisioner/global_stream.go

@@ -255,6 +255,59 @@ func GlobalStreamListener(
 
 					cluster, err := repo.Cluster.CreateCluster(cluster)
 
+					if err != nil {
+						continue
+					}
+				} else if kind == string(models.InfraDOCR) {
+					reg := &models.Registry{
+						ProjectID:       projID,
+						DOIntegrationID: infra.DOIntegrationID,
+						InfraID:         infra.ID,
+					}
+
+					// parse raw data into DOCR type
+					dataString, ok := msg.Values["data"].(string)
+
+					if ok {
+						json.Unmarshal([]byte(dataString), reg)
+					}
+
+					reg, err = repo.Registry.CreateRegistry(reg)
+
+					if err != nil {
+						continue
+					}
+				} else if kind == string(models.InfraDOKS) {
+					cluster := &models.Cluster{
+						AuthMechanism:   models.DO,
+						ProjectID:       projID,
+						DOIntegrationID: infra.DOIntegrationID,
+						InfraID:         infra.ID,
+					}
+
+					// parse raw data into GKE type
+					dataString, ok := msg.Values["data"].(string)
+
+					if ok {
+						json.Unmarshal([]byte(dataString), cluster)
+					}
+
+					re := regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`)
+
+					// if it matches the base64 regex, decode it
+					caData := string(cluster.CertificateAuthorityData)
+					if re.MatchString(caData) {
+						decoded, err := base64.StdEncoding.DecodeString(caData)
+
+						if err != nil {
+							continue
+						}
+
+						cluster.CertificateAuthorityData = []byte(decoded)
+					}
+
+					cluster, err := repo.Cluster.CreateCluster(cluster)
+
 					if err != nil {
 						continue
 					}

+ 18 - 0
internal/kubernetes/provisioner/provisioner.go

@@ -10,6 +10,9 @@ import (
 	"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"
@@ -27,6 +30,8 @@ const (
 	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
@@ -50,6 +55,11 @@ type Conf struct {
 	// GKE
 	GCP *gcp.Conf
 	GKE *gke.Conf
+
+	// DO
+	DO   *do.Conf
+	DOCR *docr.Conf
+	DOKS *doks.Conf
 }
 
 type ProvisionerOperation string
@@ -103,6 +113,14 @@ func (conf *Conf) GetProvisionerJobTemplate() (*batchv1.Job, error) {
 		args = []string{operation, "gke"}
 		env = conf.GCP.AttachGCPEnv(env)
 		env = conf.GKE.AttachGKEEnv(env)
+	} else if conf.Kind == DOCR {
+		args = []string{operation, "docr"}
+		env = conf.DO.AttachDOEnv(env)
+		env = conf.DOCR.AttachDOCREnv(env)
+	} else if conf.Kind == GKE {
+		args = []string{operation, "doks"}
+		env = conf.DO.AttachDOEnv(env)
+		env = conf.DOKS.AttachDOKSEnv(env)
 	}
 
 	return &batchv1.Job{

+ 2 - 0
internal/models/cluster.go

@@ -18,6 +18,7 @@ const (
 	OIDC   ClusterAuth = "oidc"
 	GCP    ClusterAuth = "gcp-sa"
 	AWS    ClusterAuth = "aws-sa"
+	DO     ClusterAuth = "do-oauth"
 	Local  ClusterAuth = "local"
 )
 
@@ -58,6 +59,7 @@ type Cluster struct {
 	OIDCIntegrationID uint
 	GCPIntegrationID  uint
 	AWSIntegrationID  uint
+	DOIntegrationID   uint
 
 	// A token cache that can be used by an auth mechanism, if desired
 	TokenCache integrations.ClusterTokenCache `json:"token_cache"`

+ 10 - 4
internal/models/infra.go

@@ -25,10 +25,12 @@ type InfraKind string
 
 // The supported infra kinds
 const (
-	InfraECR InfraKind = "ecr"
-	InfraEKS InfraKind = "eks"
-	InfraGCR InfraKind = "gcr"
-	InfraGKE InfraKind = "gke"
+	InfraECR  InfraKind = "ecr"
+	InfraEKS  InfraKind = "eks"
+	InfraGCR  InfraKind = "gcr"
+	InfraGKE  InfraKind = "gke"
+	InfraDOCR InfraKind = "docr"
+	InfraDOKS InfraKind = "doks"
 )
 
 // Infra represents the metadata for an infrastructure type provisioned on
@@ -53,6 +55,10 @@ type Infra struct {
 
 	// The GCP integration that was used to create the infra
 	GCPIntegrationID uint
+
+	// The DO integration that was used to create the infra:
+	// this points to an OAuthIntegrationID
+	DOIntegrationID uint
 }
 
 // InfraExternal is an external Infra to be shared over REST

+ 40 - 1
internal/models/integrations/gcp.go

@@ -2,6 +2,7 @@ package integrations
 
 import (
 	"context"
+	"encoding/json"
 
 	"golang.org/x/oauth2/google"
 	"gorm.io/gorm"
@@ -81,6 +82,7 @@ func (g *GCPIntegration) ToProjectIntegration(
 func (g *GCPIntegration) GetBearerToken(
 	getTokenCache GetTokenCacheFunc,
 	setTokenCache SetTokenCacheFunc,
+	scopes ...string,
 ) (string, error) {
 	cache, err := getTokenCache()
 
@@ -94,7 +96,7 @@ func (g *GCPIntegration) GetBearerToken(
 	creds, err := google.CredentialsFromJSON(
 		context.Background(),
 		g.GCPKeyData,
-		"https://www.googleapis.com/auth/cloud-platform",
+		scopes...,
 	)
 
 	if err != nil {
@@ -112,3 +114,40 @@ func (g *GCPIntegration) GetBearerToken(
 
 	return tok.AccessToken, nil
 }
+
+// credentialsFile is the unmarshalled representation of a GCP credentials file.
+// Source; golang.org/x/oauth2/google
+type credentialsFile struct {
+	Type string `json:"type"` // serviceAccountKey or userCredentialsKey
+
+	// Service Account fields
+	ClientEmail  string `json:"client_email"`
+	PrivateKeyID string `json:"private_key_id"`
+	PrivateKey   string `json:"private_key"`
+	TokenURL     string `json:"token_uri"`
+	ProjectID    string `json:"project_id"`
+
+	// User Credential fields
+	// (These typically come from gcloud auth.)
+	ClientSecret string `json:"client_secret"`
+	ClientID     string `json:"client_id"`
+	RefreshToken string `json:"refresh_token"`
+
+	// External Account fields
+	Audience                       string `json:"audience"`
+	SubjectTokenType               string `json:"subject_token_type"`
+	TokenURLExternal               string `json:"token_url"`
+	TokenInfoURL                   string `json:"token_info_url"`
+	ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
+	// CredentialSource               externalaccount.CredentialSource `json:"credential_source"`
+	QuotaProjectID string `json:"quota_project_id"`
+}
+
+func GCPProjectIDFromJSON(jsonData []byte) (string, error) {
+	var f credentialsFile
+	if err := json.Unmarshal(jsonData, &f); err != nil {
+		return "", err
+	}
+
+	return f.ProjectID, nil
+}

+ 1 - 0
internal/models/integrations/integration.go

@@ -13,6 +13,7 @@ const (
 	Kube     IntegrationService = "kube"
 	GCR      IntegrationService = "gcr"
 	ECR      IntegrationService = "ecr"
+	DOCR     IntegrationService = "docr"
 	Github   IntegrationService = "github"
 	Docker   IntegrationService = "docker"
 )

+ 5 - 2
internal/models/integrations/oauth.go

@@ -1,13 +1,16 @@
 package integrations
 
-import "gorm.io/gorm"
+import (
+	"gorm.io/gorm"
+)
 
 // OAuthIntegrationClient is the name of an OAuth mechanism client
 type OAuthIntegrationClient string
 
 // The supported oauth mechanism clients
 const (
-	OAuthGithub OAuthIntegrationClient = "github"
+	OAuthGithub       OAuthIntegrationClient = "github"
+	OAuthDigitalOcean OAuthIntegrationClient = "do"
 )
 
 // OAuthIntegration is an auth mechanism that uses oauth

+ 3 - 0
internal/models/registry.go

@@ -28,6 +28,7 @@ type Registry struct {
 
 	GCPIntegrationID uint
 	AWSIntegrationID uint
+	DOIntegrationID  uint
 
 	// A token cache that can be used by an auth mechanism (integration), if desired
 	TokenCache integrations.RegTokenCache
@@ -61,6 +62,8 @@ func (r *Registry) Externalize() *RegistryExternal {
 		serv = integrations.ECR
 	} else if r.GCPIntegrationID != 0 {
 		serv = integrations.GCR
+	} else if r.DOIntegrationID != 0 {
+		serv = integrations.DOCR
 	}
 
 	return &RegistryExternal{

+ 50 - 0
internal/oauth/config.go

@@ -1,9 +1,13 @@
 package oauth
 
 import (
+	"context"
 	"crypto/rand"
 	"encoding/base64"
+	"time"
 
+	"github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
 	"golang.org/x/oauth2"
 )
 
@@ -27,6 +31,19 @@ func NewGithubClient(cfg *Config) *oauth2.Config {
 	}
 }
 
+func NewDigitalOceanClient(cfg *Config) *oauth2.Config {
+	return &oauth2.Config{
+		ClientID:     cfg.ClientID,
+		ClientSecret: cfg.ClientSecret,
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  "https://cloud.digitalocean.com/v1/oauth/authorize",
+			TokenURL: "https://cloud.digitalocean.com/v1/oauth/token",
+		},
+		RedirectURL: cfg.BaseURL + "/api/oauth/digitalocean/callback",
+		Scopes:      cfg.Scopes,
+	}
+}
+
 func CreateRandomState() string {
 	b := make([]byte, 16)
 	rand.Read(b)
@@ -35,3 +52,36 @@ func CreateRandomState() string {
 
 	return state
 }
+
+// GetAccessToken retrieves an access token for a given client. It updates the
+// access token in the DB if necessary
+func GetAccessToken(
+	o *integrations.OAuthIntegration,
+	conf *oauth2.Config,
+	repo repository.Repository,
+) (string, *time.Time, error) {
+	tokSource := conf.TokenSource(context.TODO(), &oauth2.Token{
+		AccessToken:  string(o.AccessToken),
+		RefreshToken: string(o.RefreshToken),
+		TokenType:    "Bearer",
+	})
+
+	token, err := tokSource.Token()
+
+	if err != nil {
+		return "", nil, err
+	}
+
+	if token.AccessToken != string(o.AccessToken) {
+		o.AccessToken = []byte(token.AccessToken)
+		o.RefreshToken = []byte(token.RefreshToken)
+
+		o, err = repo.OAuthIntegration.UpdateOAuthIntegration(o)
+
+		if err != nil {
+			return "", nil, err
+		}
+	}
+
+	return token.AccessToken, &token.Expiry, nil
+}

+ 275 - 2
internal/registry/registry.go

@@ -1,6 +1,8 @@
 package registry
 
 import (
+	"context"
+	"encoding/base64"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -10,9 +12,15 @@ import (
 
 	"github.com/aws/aws-sdk-go/service/ecr"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/oauth2"
 
 	ints "github.com/porter-dev/porter/internal/models/integrations"
+
+	"github.com/digitalocean/godo"
+	"github.com/docker/cli/cli/config/configfile"
+	"github.com/docker/cli/cli/config/types"
 )
 
 // Registry wraps the gorm Registry model
@@ -46,7 +54,10 @@ type Image struct {
 }
 
 // ListRepositories lists the repositories for a registry
-func (r *Registry) ListRepositories(repo repository.Repository) ([]*Repository, error) {
+func (r *Registry) ListRepositories(
+	repo repository.Repository,
+	doAuth *oauth2.Config, // only required if using DOCR
+) ([]*Repository, error) {
 	// switch on the auth mechanism to get a token
 	if r.AWSIntegrationID != 0 {
 		return r.listECRRepositories(repo)
@@ -56,6 +67,10 @@ func (r *Registry) ListRepositories(repo repository.Repository) ([]*Repository,
 		return r.listGCRRepositories(repo)
 	}
 
+	if r.DOIntegrationID != 0 {
+		return r.listDOCRRepositories(repo, doAuth)
+	}
+
 	return nil, fmt.Errorf("error listing repositories")
 }
 
@@ -78,7 +93,11 @@ func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, er
 	}
 
 	// get oauth2 access token
-	_, err = gcp.GetBearerToken(r.getTokenCache, r.setTokenCacheFunc(repo))
+	_, err = gcp.GetBearerToken(
+		r.getTokenCache,
+		r.setTokenCacheFunc(repo),
+		"https://www.googleapis.com/auth/devstorage.read_write",
+	)
 
 	if err != nil {
 		return nil, err
@@ -187,6 +206,52 @@ func (r *Registry) listECRRepositories(repo repository.Repository) ([]*Repositor
 	return res, nil
 }
 
+func (r *Registry) listDOCRRepositories(
+	repo repository.Repository,
+	doAuth *oauth2.Config,
+) ([]*Repository, error) {
+	oauthInt, err := repo.OAuthIntegration.ReadOAuthIntegration(
+		r.DOIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	tok, _, err := oauth.GetAccessToken(oauthInt, doAuth, repo)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client := godo.NewFromToken(tok)
+
+	urlArr := strings.Split(r.URL, "/")
+
+	if len(urlArr) != 2 {
+		return nil, fmt.Errorf("invalid digital ocean registry url")
+	}
+
+	name := urlArr[1]
+
+	repos, _, err := client.Registry.ListRepositories(context.TODO(), name, &godo.ListOptions{})
+
+	if err != nil {
+		return nil, err
+	}
+
+	res := make([]*Repository, 0)
+
+	for _, repo := range repos {
+		res = append(res, &Repository{
+			Name: repo.Name,
+			URI:  r.URL + "/" + repo.Name,
+		})
+	}
+
+	return res, nil
+}
+
 func (r *Registry) getTokenCache() (tok *ints.TokenCache, err error) {
 	return &ints.TokenCache{
 		Token:  r.TokenCache.Token,
@@ -216,6 +281,7 @@ func (r *Registry) setTokenCacheFunc(
 func (r *Registry) ListImages(
 	repoName string,
 	repo repository.Repository,
+	doAuth *oauth2.Config, // only required if using DOCR
 ) ([]*Image, error) {
 	// switch on the auth mechanism to get a token
 	if r.AWSIntegrationID != 0 {
@@ -226,6 +292,10 @@ func (r *Registry) ListImages(
 		return r.listGCRImages(repoName, repo)
 	}
 
+	if r.DOIntegrationID != 0 {
+		return r.listDOCRImages(repoName, repo, doAuth)
+	}
+
 	return nil, fmt.Errorf("error listing images")
 }
 
@@ -326,3 +396,206 @@ func (r *Registry) listGCRImages(repoName string, repo repository.Repository) ([
 
 	return res, nil
 }
+
+func (r *Registry) listDOCRImages(
+	repoName string,
+	repo repository.Repository,
+	doAuth *oauth2.Config,
+) ([]*Image, error) {
+	oauthInt, err := repo.OAuthIntegration.ReadOAuthIntegration(
+		r.DOIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	tok, _, err := oauth.GetAccessToken(oauthInt, doAuth, repo)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client := godo.NewFromToken(tok)
+
+	urlArr := strings.Split(r.URL, "/")
+
+	if len(urlArr) != 2 {
+		return nil, fmt.Errorf("invalid digital ocean registry url")
+	}
+
+	name := urlArr[1]
+
+	tags, _, err := client.Registry.ListRepositoryTags(context.TODO(), name, repoName, &godo.ListOptions{})
+
+	if err != nil {
+		return nil, err
+	}
+
+	res := make([]*Image, 0)
+
+	for _, tag := range tags {
+		res = append(res, &Image{
+			RepositoryName: repoName,
+			Tag:            tag.Tag,
+		})
+	}
+
+	return res, nil
+}
+
+// GetDockerConfigJSON returns a dockerconfigjson file contents with "auths"
+// populated.
+func (r *Registry) GetDockerConfigJSON(
+	repo repository.Repository,
+	doAuth *oauth2.Config, // only required if using DOCR
+) ([]byte, error) {
+	var conf *configfile.ConfigFile
+	var err error
+
+	// switch on the auth mechanism to get a token
+	if r.AWSIntegrationID != 0 {
+		conf, err = r.getECRDockerConfigFile(repo)
+	}
+
+	if r.GCPIntegrationID != 0 {
+		conf, err = r.getGCRDockerConfigFile(repo)
+	}
+
+	if r.DOIntegrationID != 0 {
+		conf, err = r.getDOCRDockerConfigFile(repo, doAuth)
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	return json.Marshal(conf)
+}
+
+func (r *Registry) getECRDockerConfigFile(
+	repo repository.Repository,
+) (*configfile.ConfigFile, error) {
+	aws, err := repo.AWSIntegration.ReadAWSIntegration(
+		r.AWSIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	sess, err := aws.GetSession()
+
+	if err != nil {
+		return nil, err
+	}
+
+	ecrSvc := ecr.New(sess)
+
+	output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+
+	if err != nil {
+		return nil, err
+	}
+
+	token := *output.AuthorizationData[0].AuthorizationToken
+
+	decodedToken, err := base64.StdEncoding.DecodeString(token)
+
+	if err != nil {
+		return nil, err
+	}
+
+	parts := strings.SplitN(string(decodedToken), ":", 2)
+
+	if len(parts) < 2 {
+		return nil, err
+	}
+
+	key := r.URL
+
+	if !strings.Contains(key, "http") {
+		key = "https://" + key
+	}
+
+	return &configfile.ConfigFile{
+		AuthConfigs: map[string]types.AuthConfig{
+			key: types.AuthConfig{
+				Username: parts[0],
+				Password: parts[1],
+				Auth:     token,
+			},
+		},
+	}, nil
+}
+
+func (r *Registry) getGCRDockerConfigFile(
+	repo repository.Repository,
+) (*configfile.ConfigFile, error) {
+	gcp, err := repo.GCPIntegration.ReadGCPIntegration(
+		r.GCPIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	key := r.URL
+
+	if !strings.Contains(key, "http") {
+		key = "https://" + key
+	}
+
+	parsedURL, _ := url.Parse(key)
+
+	return &configfile.ConfigFile{
+		AuthConfigs: map[string]types.AuthConfig{
+			parsedURL.Host: types.AuthConfig{
+				Username: "_json_key",
+				Password: string(gcp.GCPKeyData),
+				Auth:     generateAuthToken("_json_key", string(gcp.GCPKeyData)),
+			},
+		},
+	}, nil
+}
+
+func (r *Registry) getDOCRDockerConfigFile(
+	repo repository.Repository,
+	doAuth *oauth2.Config,
+) (*configfile.ConfigFile, error) {
+	oauthInt, err := repo.OAuthIntegration.ReadOAuthIntegration(
+		r.DOIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	tok, _, err := oauth.GetAccessToken(oauthInt, doAuth, repo)
+
+	if err != nil {
+		return nil, err
+	}
+
+	key := r.URL
+
+	if !strings.Contains(key, "http") {
+		key = "https://" + key
+	}
+
+	parsedURL, _ := url.Parse(key)
+
+	return &configfile.ConfigFile{
+		AuthConfigs: map[string]types.AuthConfig{
+			parsedURL.Host: types.AuthConfig{
+				Username: tok,
+				Password: tok,
+				Auth:     generateAuthToken(tok, tok),
+			},
+		},
+	}, nil
+}
+
+func generateAuthToken(username, password string) string {
+	return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
+}

+ 23 - 0
internal/repository/gorm/auth.go

@@ -670,6 +670,29 @@ func (repo *OAuthIntegrationRepository) ListOAuthIntegrationsByProjectID(
 	return oauths, nil
 }
 
+// UpdateOAuthIntegration modifies an existing oauth integration in the database
+func (repo *OAuthIntegrationRepository) UpdateOAuthIntegration(
+	am *ints.OAuthIntegration,
+) (*ints.OAuthIntegration, error) {
+	err := repo.EncryptOAuthIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if err := repo.db.Save(am).Error; err != nil {
+		return nil, err
+	}
+
+	err = repo.DecryptOAuthIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return am, nil
+}
+
 // EncryptOAuthIntegrationData will encrypt the oauth integration data before
 // writing to the DB
 func (repo *OAuthIntegrationRepository) EncryptOAuthIntegrationData(

+ 12 - 0
internal/repository/gorm/cluster.go

@@ -197,10 +197,22 @@ func (repo *ClusterRepository) ListClustersByProjectID(
 func (repo *ClusterRepository) UpdateCluster(
 	cluster *models.Cluster,
 ) (*models.Cluster, error) {
+	err := repo.EncryptClusterData(cluster, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
 	if err := repo.db.Save(cluster).Error; err != nil {
 		return nil, err
 	}
 
+	err = repo.DecryptClusterData(cluster, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
 	return cluster, nil
 }
 

+ 1 - 0
internal/repository/integrations.go

@@ -34,6 +34,7 @@ type OAuthIntegrationRepository interface {
 	CreateOAuthIntegration(am *ints.OAuthIntegration) (*ints.OAuthIntegration, error)
 	ReadOAuthIntegration(id uint) (*ints.OAuthIntegration, error)
 	ListOAuthIntegrationsByProjectID(projectID uint) ([]*ints.OAuthIntegration, error)
+	UpdateOAuthIntegration(am *ints.OAuthIntegration) (*ints.OAuthIntegration, error)
 }
 
 // AWSIntegrationRepository represents the set of queries on the AWS auth

+ 18 - 0
internal/repository/memory/auth.go

@@ -265,6 +265,24 @@ func (repo *OAuthIntegrationRepository) ListOAuthIntegrationsByProjectID(
 	return res, nil
 }
 
+// UpdateOAuthIntegration updates an oauth integration in the DB
+func (repo *OAuthIntegrationRepository) UpdateOAuthIntegration(
+	am *ints.OAuthIntegration,
+) (*ints.OAuthIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if int(am.ID-1) >= len(repo.oIntegrations) || repo.oIntegrations[am.ID-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(am.ID - 1)
+	repo.oIntegrations[index] = am
+
+	return am, nil
+}
+
 // AWSIntegrationRepository implements repository.AWSIntegrationRepository
 type AWSIntegrationRepository struct {
 	canQuery        bool

+ 7 - 2
internal/templater/helm/values/writer.go

@@ -42,7 +42,7 @@ func (w *TemplateWriter) Create(
 		Values:    vals,
 	}
 
-	_, err := w.Agent.InstallChart(conf)
+	_, err := w.Agent.InstallChart(conf, nil)
 
 	if err != nil {
 		return nil, err
@@ -59,7 +59,12 @@ func (w *TemplateWriter) Update(
 		return nil, fmt.Errorf("release not set")
 	}
 
-	_, err := w.Agent.UpgradeReleaseByValues(w.ReleaseName, vals)
+	conf := &helm.UpgradeReleaseConfig{
+		Name:   w.ReleaseName,
+		Values: vals,
+	}
+
+	_, err := w.Agent.UpgradeReleaseByValues(conf, nil)
 
 	if err != nil {
 		return nil, err

+ 10 - 0
server/api/api.go

@@ -69,6 +69,7 @@ type App struct {
 
 	// oauth-specific clients
 	GithubConf *oauth2.Config
+	DOConf     *oauth2.Config
 
 	db         *gorm.DB
 	validator  *vr.Validate
@@ -124,5 +125,14 @@ func New(conf *AppConfig) (*App, error) {
 		})
 	}
 
+	if sc := conf.ServerConf; sc.DOClientID != "" && sc.DOClientSecret != "" {
+		app.DOConf = oauth.NewDigitalOceanClient(&oauth.Config{
+			ClientID:     sc.DOClientID,
+			ClientSecret: sc.DOClientSecret,
+			Scopes:       []string{"read", "write"},
+			BaseURL:      sc.ServerURL,
+		})
+	}
+
 	return app, nil
 }

+ 25 - 6
server/api/deploy_handler.go

@@ -5,6 +5,7 @@ import (
 	"math/rand"
 	"net/http"
 	"net/url"
+	"strconv"
 
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/internal/forms"
@@ -15,6 +16,13 @@ import (
 
 // HandleDeployTemplate triggers a chart deployment from a template
 func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
 	name := chi.URLParam(r, "name")
 	version := chi.URLParam(r, "version")
 
@@ -49,7 +57,8 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	form := &forms.InstallChartTemplateForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		ChartTemplateForm: &forms.ChartTemplateForm{},
@@ -76,14 +85,24 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	registries, err := app.Repo.Registry.ListRegistriesByProjectID(uint(projID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
 	conf := &helm.InstallChartConfig{
-		Chart:     chart,
-		Name:      form.ChartTemplateForm.Name,
-		Namespace: form.ReleaseForm.Form.Namespace,
-		Values:    form.ChartTemplateForm.FormValues,
+		Chart:      chart,
+		Name:       form.ChartTemplateForm.Name,
+		Namespace:  form.ReleaseForm.Form.Namespace,
+		Values:     form.ChartTemplateForm.FormValues,
+		Cluster:    form.ReleaseForm.Cluster,
+		Repo:       *app.Repo,
+		Registries: registries,
 	}
 
-	_, err = agent.InstallChart(conf)
+	_, err = agent.InstallChart(conf, app.DOConf)
 
 	if err != nil {
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{

+ 31 - 0
server/api/integration_handler.go

@@ -8,6 +8,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/internal/forms"
 
+	"github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 )
 
@@ -251,3 +252,33 @@ func (app *App) HandleCreateBasicAuthIntegration(w http.ResponseWriter, r *http.
 		return
 	}
 }
+
+// HandleListProjectOAuthIntegrations lists the oauth integrations for the project
+func (app *App) HandleListProjectOAuthIntegrations(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	oauthInts, err := app.Repo.OAuthIntegration.ListOAuthIntegrationsByProjectID(uint(projID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	res := make([]*integrations.OAuthIntegrationExternal, 0)
+
+	for _, oauthInt := range oauthInts {
+		res = append(res, oauthInt.Externalize())
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(res); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}

+ 10 - 5
server/api/k8s_handler.go

@@ -35,7 +35,8 @@ func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -95,7 +96,8 @@ func (app *App) HandleGetPodLogs(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -158,7 +160,8 @@ func (app *App) HandleGetIngress(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -214,7 +217,8 @@ func (app *App) HandleListPods(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -277,7 +281,8 @@ func (app *App) HandleStreamControllerStatus(w http.ResponseWriter, r *http.Requ
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 

+ 99 - 0
server/api/oauth_do_handler.go

@@ -0,0 +1,99 @@
+package api
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/internal/oauth"
+	"golang.org/x/oauth2"
+
+	"github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// HandleDOOAuthStartProject starts the oauth2 flow for a project digitalocean request.
+// In this handler, the project id gets written to the session (along with the oauth
+// state param), so that the correct project id can be identified in the callback.
+func (app *App) HandleDOOAuthStartProject(w http.ResponseWriter, r *http.Request) {
+	state := oauth.CreateRandomState()
+
+	err := app.populateOAuthSession(w, r, state, true)
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// specify access type offline to get a refresh token
+	url := app.DOConf.AuthCodeURL(state, oauth2.AccessTypeOffline)
+
+	http.Redirect(w, r, url, 302)
+}
+
+// HandleDOOAuthCallback verifies the callback request by checking that the
+// state parameter has not been modified, and validates the token.
+func (app *App) HandleDOOAuthCallback(w http.ResponseWriter, r *http.Request) {
+	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	if _, ok := session.Values["state"]; !ok {
+		app.sendExternalError(
+			err,
+			http.StatusForbidden,
+			HTTPError{
+				Code: http.StatusForbidden,
+				Errors: []string{
+					"Could not read cookie: are cookies enabled?",
+				},
+			},
+			w,
+		)
+
+		return
+	}
+
+	if r.URL.Query().Get("state") != session.Values["state"] {
+		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+		return
+	}
+
+	token, err := app.DOConf.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
+
+	if err != nil {
+		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+		return
+	}
+
+	if !token.Valid() {
+		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+		return
+	}
+
+	userID, _ := session.Values["user_id"].(uint)
+	projID, _ := session.Values["project_id"].(uint)
+
+	oauthInt := &integrations.OAuthIntegration{
+		Client:       integrations.OAuthDigitalOcean,
+		UserID:       userID,
+		ProjectID:    projID,
+		AccessToken:  []byte(token.AccessToken),
+		RefreshToken: []byte(token.RefreshToken),
+	}
+
+	// create the oauth integration first
+	oauthInt, err = app.Repo.OAuthIntegration.CreateOAuthIntegration(oauthInt)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	if session.Values["query_params"] != "" {
+		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+	} else {
+		http.Redirect(w, r, "/dashboard", 302)
+	}
+}

+ 1 - 1
server/api/oauth_github_handler.go

@@ -139,7 +139,7 @@ func (app *App) populateOAuthSession(w http.ResponseWriter, r *http.Request, sta
 			return fmt.Errorf("could not read project id")
 		}
 
-		session.Values["project_id"] = projID
+		session.Values["project_id"] = uint(projID)
 		session.Values["query_params"] = r.URL.RawQuery
 	}
 

+ 356 - 0
server/api/provision_handler.go

@@ -706,3 +706,359 @@ func (app *App) HandleGetProvisioningLogs(w http.ResponseWriter, r *http.Request
 		return
 	}
 }
+
+// HandleProvisionDODOCRInfra provisions a new digitalocean DOCR instance for a project
+func (app *App) HandleProvisionDODOCRInfra(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.CreateDOCRInfra{
+		ProjectID: uint(projID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to an aws infra instance
+	infra, err := form.ToInfra()
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	infra, err = app.Repo.Infra.CreateInfra(infra)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	oauthInt, err := app.Repo.OAuthIntegration.ReadOAuthIntegration(infra.DOIntegrationID)
+
+	if err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// launch provisioning pod
+	agent, err := kubernetes.GetAgentInClusterConfig()
+
+	if err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	_, err = agent.ProvisionDOCR(
+		uint(projID),
+		oauthInt,
+		app.DOConf,
+		*app.Repo,
+		form.DOCRName,
+		form.DOCRSubscriptionTier,
+		infra,
+		provisioner.Apply,
+		&app.DBConf,
+		app.RedisConf,
+	)
+
+	if err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	app.Logger.Info().Msgf("New do docr infra created: %d", infra.ID)
+
+	w.WriteHeader(http.StatusCreated)
+
+	infraExt := infra.Externalize()
+
+	if err := json.NewEncoder(w).Encode(infraExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// HandleDestroyAWSDOCRInfra destroys docr infra
+func (app *App) HandleDestroyDODOCRInfra(w http.ResponseWriter, r *http.Request) {
+	// get path parameters
+	infraID, err := strconv.ParseUint(chi.URLParam(r, "infra_id"), 10, 64)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// read infra to get id
+	infra, err := app.Repo.Infra.ReadInfra(uint(infraID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	oauthInt, err := app.Repo.OAuthIntegration.ReadOAuthIntegration(infra.DOIntegrationID)
+
+	form := &forms.DestroyDOCRInfra{}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// launch provisioning destruction pod
+	agent, err := kubernetes.GetAgentInClusterConfig()
+
+	if err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// mark infra for deletion
+	infra.Status = models.StatusDestroying
+	infra, err = app.Repo.Infra.UpdateInfra(infra)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	_, err = agent.ProvisionDOCR(
+		infra.ProjectID,
+		oauthInt,
+		app.DOConf,
+		*app.Repo,
+		form.DOCRName,
+		"basic", // this doesn't matter for destroy
+		infra,
+		provisioner.Destroy,
+		&app.DBConf,
+		app.RedisConf,
+	)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	app.Logger.Info().Msgf("DO DOCR infra marked for destruction: %d", infra.ID)
+
+	w.WriteHeader(http.StatusOK)
+}
+
+// HandleProvisionDODOKSInfra provisions a new DO DOKS instance for a project
+func (app *App) HandleProvisionDODOKSInfra(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.CreateDOKSInfra{
+		ProjectID: uint(projID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to an aws infra instance
+	infra, err := form.ToInfra()
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	infra, err = app.Repo.Infra.CreateInfra(infra)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	oauthInt, err := app.Repo.OAuthIntegration.ReadOAuthIntegration(infra.DOIntegrationID)
+
+	if err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// launch provisioning pod
+	agent, err := kubernetes.GetAgentInClusterConfig()
+
+	if err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	_, err = agent.ProvisionDOKS(
+		uint(projID),
+		oauthInt,
+		app.DOConf,
+		*app.Repo,
+		form.DORegion,
+		form.DOKSName,
+		infra,
+		provisioner.Apply,
+		&app.DBConf,
+		app.RedisConf,
+	)
+
+	if err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	app.Logger.Info().Msgf("New do doks infra created: %d", infra.ID)
+
+	w.WriteHeader(http.StatusCreated)
+
+	infraExt := infra.Externalize()
+
+	if err := json.NewEncoder(w).Encode(infraExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// HandleDestroyDODOKSInfra destroys DOKS infra
+func (app *App) HandleDestroyDODOKSInfra(w http.ResponseWriter, r *http.Request) {
+	// get path parameters
+	infraID, err := strconv.ParseUint(chi.URLParam(r, "infra_id"), 10, 64)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// read infra to get id
+	infra, err := app.Repo.Infra.ReadInfra(uint(infraID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	oauthInt, err := app.Repo.OAuthIntegration.ReadOAuthIntegration(infra.DOIntegrationID)
+
+	form := &forms.DestroyDOKSInfra{}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// launch provisioning destruction pod
+	agent, err := kubernetes.GetAgentInClusterConfig()
+
+	if err != nil {
+		infra.Status = models.StatusError
+		infra, _ = app.Repo.Infra.UpdateInfra(infra)
+
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// mark infra for deletion
+	infra.Status = models.StatusDestroying
+	infra, err = app.Repo.Infra.UpdateInfra(infra)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	_, err = agent.ProvisionDOKS(
+		infra.ProjectID,
+		oauthInt,
+		app.DOConf,
+		*app.Repo,
+		"nyc1",
+		form.DOKSName,
+		infra,
+		provisioner.Destroy,
+		&app.DBConf,
+		app.RedisConf,
+	)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	app.Logger.Info().Msgf("DO DOKS infra marked for destruction: %d", infra.ID)
+
+	w.WriteHeader(http.StatusOK)
+}

+ 62 - 2
server/api/registry_handler.go

@@ -7,6 +7,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/porter-dev/porter/internal/oauth"
+
 	"github.com/porter-dev/porter/internal/registry"
 
 	"github.com/go-chi/chi"
@@ -211,6 +213,64 @@ func (app *App) HandleGetProjectRegistryGCRToken(w http.ResponseWriter, r *http.
 
 			token = string(tokenCache.Token)
 			expiresAt = &tokenCache.Expiry
+			break
+		}
+	}
+
+	resp := &RegTokenResponse{
+		Token:     token,
+		ExpiresAt: expiresAt,
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// HandleGetProjectRegistryDOCRToken gets a DOCR token for a registry
+func (app *App) HandleGetProjectRegistryDOCRToken(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	reqBody := &GCRTokenRequestBody{}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(reqBody); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// list registries and find one that matches the region
+	regs, err := app.Repo.Registry.ListRegistriesByProjectID(uint(projID))
+	var token string
+	var expiresAt *time.Time
+
+	for _, reg := range regs {
+		if reg.DOIntegrationID != 0 && strings.Contains(reg.URL, reqBody.ServerURL) {
+			oauthInt, err := app.Repo.OAuthIntegration.ReadOAuthIntegration(reg.DOIntegrationID)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			tok, expiry, err := oauth.GetAccessToken(oauthInt, app.DOConf, *app.Repo)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			token = tok
+			expiresAt = expiry
+			break
 		}
 	}
 
@@ -331,7 +391,7 @@ func (app *App) HandleListRepositories(w http.ResponseWriter, r *http.Request) {
 	_reg := registry.Registry(*reg)
 	regAPI := &_reg
 
-	repos, err := regAPI.ListRepositories(*app.Repo)
+	repos, err := regAPI.ListRepositories(*app.Repo, app.DOConf)
 
 	if err != nil {
 		app.handleErrorRead(err, ErrProjectDataRead, w)
@@ -368,7 +428,7 @@ func (app *App) HandleListImages(w http.ResponseWriter, r *http.Request) {
 	_reg := registry.Registry(*reg)
 	regAPI := &_reg
 
-	imgs, err := regAPI.ListImages(repoName, *app.Repo)
+	imgs, err := regAPI.ListImages(repoName, *app.Repo, app.DOConf)
 
 	if err != nil {
 		app.handleErrorRead(err, ErrProjectDataRead, w)

+ 76 - 14
server/api/release_handler.go

@@ -34,7 +34,8 @@ func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
 	form := &forms.ListReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		ListFilter: &helm.ListFilter{},
@@ -80,7 +81,8 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name:     name,
@@ -113,7 +115,8 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	k8sForm := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -187,7 +190,8 @@ func (app *App) HandleGetReleaseComponents(w http.ResponseWriter, r *http.Reques
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name:     name,
@@ -243,7 +247,8 @@ func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Reque
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name:     name,
@@ -283,7 +288,8 @@ func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Reque
 	// get the filter options
 	k8sForm := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -369,7 +375,8 @@ func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request)
 	form := &forms.ListReleaseHistoryForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: name,
@@ -430,6 +437,13 @@ func (app *App) HandleGetReleaseToken(w http.ResponseWriter, r *http.Request) {
 
 // HandleUpgradeRelease upgrades a release with new values.yaml
 func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
 	name := chi.URLParam(r, "name")
 
 	vals, err := url.ParseQuery(r.URL.RawQuery)
@@ -442,7 +456,8 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 	form := &forms.UpgradeReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: name,
@@ -469,7 +484,21 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	_, err = agent.UpgradeRelease(form.Name, form.Values)
+	registries, err := app.Repo.Registry.ListRegistriesByProjectID(uint(projID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	conf := &helm.UpgradeReleaseConfig{
+		Name:       form.Name,
+		Cluster:    form.ReleaseForm.Cluster,
+		Repo:       *app.Repo,
+		Registries: registries,
+	}
+
+	_, err = agent.UpgradeRelease(conf, form.Values, app.DOConf)
 
 	if err != nil {
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
@@ -499,7 +528,8 @@ func (app *App) HandleReleaseDeployHook(w http.ResponseWriter, r *http.Request)
 	form := &forms.UpgradeReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: name,
@@ -533,7 +563,22 @@ func (app *App) HandleReleaseDeployHook(w http.ResponseWriter, r *http.Request)
 	newval := map[string]interface{}{}
 	newval["image"] = image
 
-	_, err = agent.UpgradeReleaseByValues(form.Name, newval)
+	registries, err := app.Repo.Registry.ListRegistriesByProjectID(uint(form.ReleaseForm.Cluster.ProjectID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	conf := &helm.UpgradeReleaseConfig{
+		Name:       form.Name,
+		Cluster:    form.ReleaseForm.Cluster,
+		Repo:       *app.Repo,
+		Registries: registries,
+		Values:     newval,
+	}
+
+	_, err = agent.UpgradeReleaseByValues(conf, app.DOConf)
 
 	if err != nil {
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
@@ -581,7 +626,8 @@ func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Reques
 	form := &forms.UpgradeReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: release.Name,
@@ -610,7 +656,22 @@ func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Reques
 	newval := map[string]interface{}{}
 	newval["image"] = image
 
-	_, err = agent.UpgradeReleaseByValues(form.Name, newval)
+	registries, err := app.Repo.Registry.ListRegistriesByProjectID(uint(form.ReleaseForm.Cluster.ProjectID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	conf := &helm.UpgradeReleaseConfig{
+		Name:       form.Name,
+		Cluster:    form.ReleaseForm.Cluster,
+		Repo:       *app.Repo,
+		Registries: registries,
+		Values:     newval,
+	}
+
+	_, err = agent.UpgradeReleaseByValues(conf, app.DOConf)
 
 	if err != nil {
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
@@ -638,7 +699,8 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 	form := &forms.RollbackReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: name,

+ 104 - 0
server/router/middleware/auth.go

@@ -90,6 +90,10 @@ type bodyGCPIntegrationID struct {
 	GCPIntegrationID uint64 `json:"gcp_integration_id"`
 }
 
+type bodyDOIntegrationID struct {
+	DOIntegrationID uint64 `json:"do_integration_id"`
+}
+
 // DoesUserIDMatch checks the id URL parameter and verifies that it matches
 // the one stored in the session
 func (auth *Auth) DoesUserIDMatch(next http.Handler, loc IDLocation) http.Handler {
@@ -485,6 +489,61 @@ func (auth *Auth) DoesUserHaveGCPIntegrationAccess(
 	})
 }
 
+// DoesUserHaveDOIntegrationAccess looks for a project_id parameter and an
+// do_integration_id parameter, and verifies that the infra belongs
+// to the project
+func (auth *Auth) DoesUserHaveDOIntegrationAccess(
+	next http.Handler,
+	projLoc IDLocation,
+	doLoc IDLocation,
+	optional bool,
+) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		doID, err := findDOIntegrationIDInRequest(r, doLoc)
+
+		if doID == 0 && optional {
+			next.ServeHTTP(w, r)
+			return
+		}
+
+		if doID == 0 || err != nil {
+			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+			return
+		}
+
+		projID, err := findProjIDInRequest(r, projLoc)
+
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+			return
+		}
+
+		oauthInts, err := auth.repo.OAuthIntegration.ListOAuthIntegrationsByProjectID(uint(projID))
+
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+			return
+		}
+
+		doesExist := false
+
+		for _, oauthInt := range oauthInts {
+			if oauthInt.ID == uint(doID) {
+				doesExist = true
+				break
+			}
+		}
+
+		if doesExist {
+			next.ServeHTTP(w, r)
+			return
+		}
+
+		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+		return
+	})
+}
+
 // Helpers
 func (auth *Auth) doesSessionMatchID(r *http.Request, id uint) bool {
 	session, _ := auth.store.Get(r, auth.cookieName)
@@ -871,3 +930,48 @@ func findGCPIntegrationIDInRequest(r *http.Request, gcpLoc IDLocation) (uint64,
 
 	return gcpID, nil
 }
+
+func findDOIntegrationIDInRequest(r *http.Request, doLoc IDLocation) (uint64, error) {
+	var doID uint64
+	var err error
+
+	if doLoc == URLParam {
+		doID, err = strconv.ParseUint(chi.URLParam(r, "do_integration_id"), 0, 64)
+
+		if err != nil {
+			return 0, err
+		}
+	} else if doLoc == BodyParam {
+		form := &bodyDOIntegrationID{}
+		body, err := ioutil.ReadAll(r.Body)
+
+		if err != nil {
+			return 0, err
+		}
+
+		err = json.Unmarshal(body, form)
+
+		if err != nil {
+			return 0, err
+		}
+
+		doID = form.DOIntegrationID
+
+		// need to create a new stream for the body
+		r.Body = ioutil.NopCloser(bytes.NewReader(body))
+	} else {
+		vals, err := url.ParseQuery(r.URL.RawQuery)
+
+		if err != nil {
+			return 0, err
+		}
+
+		if regStrArr, ok := vals["do_integration_id"]; ok && len(regStrArr) == 1 {
+			doID, err = strconv.ParseUint(regStrArr[0], 10, 64)
+		} else {
+			return 0, errors.New("do integration id not found")
+		}
+	}
+
+	return doID, nil
+}

+ 104 - 5
server/router/router.go

@@ -148,6 +148,22 @@ func New(a *api.App) *chi.Mux {
 			requestlog.NewHandler(a.HandleGithubOAuthCallback, l),
 		)
 
+		r.Method(
+			"GET",
+			"/oauth/projects/{project_id}/digitalocean",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleDOOAuthStartProject, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
+		r.Method(
+			"GET",
+			"/oauth/digitalocean/callback",
+			requestlog.NewHandler(a.HandleDOOAuthCallback, l),
+		)
+
 		// /api/projects routes
 		r.Method(
 			"GET",
@@ -207,7 +223,7 @@ func New(a *api.App) *chi.Mux {
 					requestlog.NewHandler(a.HandleProvisionAWSECRInfra, l),
 					mw.URLParam,
 					mw.BodyParam,
-					true,
+					false,
 				),
 				mw.URLParam,
 				mw.ReadAccess,
@@ -222,7 +238,7 @@ func New(a *api.App) *chi.Mux {
 					requestlog.NewHandler(a.HandleProvisionAWSEKSInfra, l),
 					mw.URLParam,
 					mw.BodyParam,
-					true,
+					false,
 				),
 				mw.URLParam,
 				mw.ReadAccess,
@@ -237,7 +253,7 @@ func New(a *api.App) *chi.Mux {
 					requestlog.NewHandler(a.HandleProvisionGCPGCRInfra, l),
 					mw.URLParam,
 					mw.BodyParam,
-					true,
+					false,
 				),
 				mw.URLParam,
 				mw.ReadAccess,
@@ -252,7 +268,37 @@ func New(a *api.App) *chi.Mux {
 					requestlog.NewHandler(a.HandleProvisionGCPGKEInfra, l),
 					mw.URLParam,
 					mw.BodyParam,
-					true,
+					false,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
+		r.Method(
+			"POST",
+			"/projects/{project_id}/provision/docr",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveDOIntegrationAccess(
+					requestlog.NewHandler(a.HandleProvisionDODOCRInfra, l),
+					mw.URLParam,
+					mw.BodyParam,
+					false,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
+		r.Method(
+			"POST",
+			"/projects/{project_id}/provision/doks",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveDOIntegrationAccess(
+					requestlog.NewHandler(a.HandleProvisionDODOKSInfra, l),
+					mw.URLParam,
+					mw.BodyParam,
+					false,
 				),
 				mw.URLParam,
 				mw.ReadAccess,
@@ -329,6 +375,34 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"POST",
+			"/projects/{project_id}/infra/{infra_id}/docr/destroy",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveInfraAccess(
+					requestlog.NewHandler(a.HandleDestroyDODOCRInfra, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
+		r.Method(
+			"POST",
+			"/projects/{project_id}/infra/{infra_id}/doks/destroy",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveInfraAccess(
+					requestlog.NewHandler(a.HandleDestroyDODOKSInfra, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
 		// /api/projects/{project_id}/clusters routes
 		r.Method(
 			"GET",
@@ -464,6 +538,16 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/integrations/oauth",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleListProjectOAuthIntegrations, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		// /api/projects/{project_id}/helmrepos routes
 		r.Method(
 			"POST",
@@ -512,7 +596,12 @@ func New(a *api.App) *chi.Mux {
 			auth.DoesUserHaveProjectAccess(
 				auth.DoesUserHaveAWSIntegrationAccess(
 					auth.DoesUserHaveGCPIntegrationAccess(
-						requestlog.NewHandler(a.HandleCreateRegistry, l),
+						auth.DoesUserHaveDOIntegrationAccess(
+							requestlog.NewHandler(a.HandleCreateRegistry, l),
+							mw.URLParam,
+							mw.BodyParam,
+							true,
+						),
 						mw.URLParam,
 						mw.BodyParam,
 						true,
@@ -570,6 +659,16 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/registries/docr/token",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleGetProjectRegistryDOCRToken, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		r.Method(
 			"DELETE",
 			"/projects/{project_id}/registries/{registry_id}",