Skip to content

Commit

Permalink
Add sweeper for projects and add retry for project deletion. (#943)
Browse files Browse the repository at this point in the history
* Add sweeper for projects.

* projects: Retry deletion until resource reassignment is complete.

* Fix flakey test.
  • Loading branch information
andrewsomething committed Feb 13, 2023
1 parent a88a19b commit 489f9ac
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 31 deletions.
38 changes: 26 additions & 12 deletions digitalocean/project/resource_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"context"
"fmt"
"log"
"net/http"
"strings"
"time"

"github.com/digitalocean/godo"
"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/util"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
Expand Down Expand Up @@ -91,6 +94,10 @@ func ResourceDigitalOceanProject() *schema.Resource {
Elem: &schema.Schema{Type: schema.TypeString},
},
},

Timeouts: &schema.ResourceTimeout{
Delete: schema.DefaultTimeout(3 * time.Minute),
},
}
}

Expand Down Expand Up @@ -253,11 +260,9 @@ func resourceDigitalOceanProjectUpdate(ctx context.Context, d *schema.ResourceDa

func resourceDigitalOceanProjectDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*config.CombinedConfig).GodoClient()

projectId := d.Id()
projectID := d.Id()

if v, ok := d.GetOk("resources"); ok {

_, err := assignResourcesToDefaultProject(client, v.(*schema.Set))
if err != nil {
return diag.Errorf("Error assigning resource to default project: %s", err)
Expand All @@ -267,19 +272,31 @@ func resourceDigitalOceanProjectDelete(ctx context.Context, d *schema.ResourceDa
log.Printf("[DEBUG] Resources assigned to default project.")
}

_, err := client.Projects.Delete(context.Background(), projectId)
// Moving resources is async and projects can not be deleted till empty. Retries may be required.
err := resource.RetryContext(ctx, d.Timeout(schema.TimeoutDelete), func() *resource.RetryError {
_, err := client.Projects.Delete(context.Background(), projectID)
if err != nil {
if util.IsDigitalOceanError(err, http.StatusPreconditionFailed, "cannot delete a project with resources") {
log.Printf("[DEBUG] Received %s, retrying project deletion", err.Error())
return resource.RetryableError(err)
}

return resource.NonRetryableError(err)
}

return nil
})
if err != nil {
return diag.Errorf("Error deleteing project %s", err)
return diag.Errorf("Error deleting project (%s): %s", projectID, err)
}

d.SetId("")
log.Printf("[INFO] Project deleted, ID: %s", projectId)
log.Printf("[INFO] Project deleted, ID: %s", projectID)

return nil
}

func assignResourcesToDefaultProject(client *godo.Client, resources *schema.Set) (*[]interface{}, error) {

defaultProject, _, defaultProjErr := client.Projects.GetDefault(context.Background())
if defaultProjErr != nil {
return nil, fmt.Errorf("Error locating default project %s", defaultProjErr)
Expand All @@ -288,12 +305,10 @@ func assignResourcesToDefaultProject(client *godo.Client, resources *schema.Set)
return assignResourcesToProject(client, defaultProject.ID, resources)
}

func assignResourcesToProject(client *godo.Client, projectId string, resources *schema.Set) (*[]interface{}, error) {

func assignResourcesToProject(client *godo.Client, projectID string, resources *schema.Set) (*[]interface{}, error) {
var urns []interface{}

for _, resource := range resources.List() {

if resource == nil {
continue
}
Expand All @@ -305,8 +320,7 @@ func assignResourcesToProject(client *godo.Client, projectId string, resources *
urns = append(urns, resource.(string))
}

_, _, err := client.Projects.AssignResources(context.Background(), projectId, urns...)

_, _, err := client.Projects.AssignResources(context.Background(), projectID, urns...)
if err != nil {
return nil, fmt.Errorf("Error assigning resources: %s", err)
}
Expand Down
36 changes: 17 additions & 19 deletions digitalocean/project/resource_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ func TestAccDigitalOceanProject_CreateWithDropletResource(t *testing.T) {
expectedDropletName := generateDropletName()

createConfig := fixtureCreateWithDropletResource(expectedDropletName, expectedName)
destroyConfig := fixtureCreateWithDefaults(expectedName)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.TestAccPreCheck(t) },
Expand All @@ -223,6 +224,18 @@ func TestAccDigitalOceanProject_CreateWithDropletResource(t *testing.T) {
resource.TestCheckResourceAttr("digitalocean_project.myproj", "resources.#", "1"),
),
},
{
Config: destroyConfig,
},
{
Config: destroyConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanProjectExists("digitalocean_project.myproj"),
resource.TestCheckResourceAttr(
"digitalocean_project.myproj", "name", expectedName),
resource.TestCheckResourceAttr("digitalocean_project.myproj", "resources.#", "0"),
),
},
},
})
}
Expand Down Expand Up @@ -267,13 +280,11 @@ func TestAccDigitalOceanProject_UpdateWithDropletResource(t *testing.T) {
}

func TestAccDigitalOceanProject_UpdateFromDropletToSpacesResource(t *testing.T) {

expectedName := generateProjectName()
expectedDropletName := generateDropletName()
expectedSpacesName := generateSpacesName()

createConfig := fixtureCreateWithDropletResource(expectedDropletName, expectedName)

updateConfig := fixtureCreateWithSpacesResource(expectedSpacesName, expectedName)

resource.ParallelTest(t, resource.TestCase{
Expand Down Expand Up @@ -315,7 +326,6 @@ func TestAccDigitalOceanProject_WithManyResources(t *testing.T) {

createConfig := fixtureCreateDomainResources(domainBase)
updateConfig := fixtureWithManyResources(domainBase, projectName)
destroyConfig := fixtureCreateWithDefaults(projectName)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.TestAccPreCheck(t) },
Expand All @@ -331,19 +341,7 @@ func TestAccDigitalOceanProject_WithManyResources(t *testing.T) {
testAccCheckDigitalOceanProjectExists("digitalocean_project.myproj"),
resource.TestCheckResourceAttr(
"digitalocean_project.myproj", "name", projectName),
resource.TestCheckResourceAttr("digitalocean_project.myproj", "resources.#", "30"),
),
},
{
Config: destroyConfig,
},
{
Config: destroyConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanProjectExists("digitalocean_project.myproj"),
resource.TestCheckResourceAttr(
"digitalocean_project.myproj", "name", projectName),
resource.TestCheckResourceAttr("digitalocean_project.myproj", "resources.#", "0"),
resource.TestCheckResourceAttr("digitalocean_project.myproj", "resources.#", "10"),
),
},
},
Expand All @@ -366,7 +364,7 @@ func testAccCheckDigitalOceanProjectResourceURNIsPresent(resource, expectedURN s

projectResources, _, err := client.Projects.ListResources(context.Background(), rs.Primary.ID, nil)
if err != nil {
return fmt.Errorf("Error Retrieving project resources to confrim.")
return fmt.Errorf("Error Retrieving project resources to confirm.")
}

for _, v := range projectResources {
Expand Down Expand Up @@ -495,15 +493,15 @@ resource "digitalocean_project" "myproj" {
func fixtureCreateDomainResources(domainBase string) string {
return fmt.Sprintf(`
resource "digitalocean_domain" "foobar" {
count = 30
count = 10
name = "%s-${count.index}.com"
}`, domainBase)
}

func fixtureWithManyResources(domainBase string, name string) string {
return fmt.Sprintf(`
resource "digitalocean_domain" "foobar" {
count = 30
count = 10
name = "%s-${count.index}.com"
}
Expand Down
58 changes: 58 additions & 0 deletions digitalocean/project/sweep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package project

import (
"context"
"log"
"net/http"
"strings"

"github.com/digitalocean/godo"
"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/sweep"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func init() {
resource.AddTestSweepers("digitalocean_project", &resource.Sweeper{
Name: "digitalocean_project",
F: sweepProjects,
Dependencies: []string{
// "digitalocean_spaces_bucket", TODO: Add when Spaces sweeper exists.
"digitalocean_droplet",
"digitalocean_domain",
},
})
}

func sweepProjects(region string) error {
meta, err := sweep.SharedConfigForRegion(region)
if err != nil {
return err
}

client := meta.(*config.CombinedConfig).GodoClient()

opt := &godo.ListOptions{PerPage: 200}
projects, _, err := client.Projects.List(context.Background(), opt)
if err != nil {
return err
}

for _, p := range projects {
if strings.HasPrefix(p.Name, sweep.TestNamePrefix) {
log.Printf("[DEBUG] Destroying project %s", p.Name)

resp, err := client.Projects.Delete(context.Background(), p.ID)
if err != nil {
// Projects with resources can not be deleted.
if resp.StatusCode == http.StatusPreconditionFailed {
log.Printf("[DEBUG] Skipping project %s: %s", p.Name, err.Error())
} else {
return err
}
}
}
}

return nil
}
1 change: 1 addition & 0 deletions digitalocean/sweep/sweep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
_ "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/image"
_ "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/kubernetes"
_ "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/loadbalancer"
_ "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/project"
_ "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/reservedip"
_ "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/snapshot"
_ "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/sshkey"
Expand Down

0 comments on commit 489f9ac

Please sign in to comment.