Procházet zdrojové kódy

provision digitalocean implementation

Alexander Belanger před 5 roky
rodič
revize
08df9a1c62

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

+ 98 - 0
internal/kubernetes/agent.go

@@ -12,10 +12,14 @@ 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"
@@ -361,6 +365,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,

+ 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
@@ -49,6 +54,11 @@ type Conf struct {
 	// GKE
 	GCP *gcp.Conf
 	GKE *gke.Conf
+
+	// DO
+	DO   *do.Conf
+	DOCR *docr.Conf
+	DOKS *doks.Conf
 }
 
 type ProvisionerOperation string
@@ -102,6 +112,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{

+ 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

+ 356 - 0
server/api/provision_handler.go

@@ -698,3 +698,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)
+}

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

+ 62 - 4
server/router/router.go

@@ -223,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,
@@ -238,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,
@@ -253,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,
@@ -268,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,
@@ -345,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",