add digitalocean_project_resources to bind resources to externally-managed projects (#396)

* add digitalocean_project_resource

* update docs for digitalocean_project_resource

* allow `resources` on digitalocean_project to be computed

* rename digitalocean_project -> digitalocean_projects

* switch to managing multiple resources

* update docs for the changes to the resource

* use Id in read function

* Simplify read method further.

Co-authored-by: Andrew Starr-Bochicchio <a.starr.b@gmail.com>
This commit is contained in:
Tom Dyas 2020-03-18 12:58:19 -07:00 committed by GitHub
parent 083c9cbfcd
commit 3a3c374c3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 347 additions and 0 deletions

View File

@ -82,6 +82,7 @@ func Provider() terraform.ResourceProvider {
"digitalocean_kubernetes_node_pool": resourceDigitalOceanKubernetesNodePool(),
"digitalocean_loadbalancer": resourceDigitalOceanLoadbalancer(),
"digitalocean_project": resourceDigitalOceanProject(),
"digitalocean_project_resources": resourceDigitalOceanProjectResources(),
"digitalocean_record": resourceDigitalOceanRecord(),
"digitalocean_spaces_bucket": resourceDigitalOceanBucket(),
"digitalocean_ssh_key": resourceDigitalOceanSSHKey(),

View File

@ -81,6 +81,7 @@ func resourceDigitalOceanProject() *schema.Resource {
"resources": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Description: "the resources associated with the project",
Elem: &schema.Schema{Type: schema.TypeString},
},

View File

@ -0,0 +1,151 @@
package digitalocean
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
)
func resourceDigitalOceanProjectResources() *schema.Resource {
return &schema.Resource{
Create: resourceDigitalOceanProjectResourcesUpdate,
Update: resourceDigitalOceanProjectResourcesUpdate,
Read: resourceDigitalOceanProjectResourcesRead,
Delete: resourceDigitalOceanProjectResourcesDelete,
Schema: map[string]*schema.Schema{
"project": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "project ID",
ValidateFunc: validation.StringIsNotEmpty,
},
"resources": {
Type: schema.TypeSet,
Required: true,
Description: "the resources associated with the project",
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}
func resourceDigitalOceanProjectResourcesUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*CombinedConfig).godoClient()
projectId := d.Get("project").(string)
_, resp, err := client.Projects.Get(context.Background(), projectId)
if err != nil {
if resp != nil && resp.StatusCode == 404 {
// Project does not exist. Mark this resource as not existing.
d.SetId("")
return nil
}
return fmt.Errorf("Error while retrieving project %s: %v", projectId, err)
}
if d.HasChange("resources") {
oldURNs, newURNs := d.GetChange("resources")
if oldURNs.(*schema.Set).Len() > 0 {
_, err = assignResourcesToDefaultProject(client, oldURNs.(*schema.Set))
if err != nil {
return fmt.Errorf("Error assigning resources to default project: %s", err)
}
}
var urns *[]interface{}
if newURNs.(*schema.Set).Len() > 0 {
urns, err = assignResourcesToProject(client, projectId, newURNs.(*schema.Set))
if err != nil {
return fmt.Errorf("Error assigning resources to project %s: %s", projectId, err)
}
}
if err = d.Set("resources", urns); err != nil {
return err
}
}
d.SetId(projectId)
return resourceDigitalOceanProjectResourcesRead(d, meta)
}
func resourceDigitalOceanProjectResourcesRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*CombinedConfig).godoClient()
projectId := d.Id()
_, resp, err := client.Projects.Get(context.Background(), projectId)
if err != nil {
if resp != nil && resp.StatusCode == 404 {
// Project does not exist. Mark this resource as not existing.
d.SetId("")
return nil
}
return fmt.Errorf("Error while retrieving project: %v", err)
}
if err = d.Set("project", projectId); err != nil {
return err
}
apiURNs, err := loadResourceURNs(client, projectId)
if err != nil {
return fmt.Errorf("Error while retrieving project resources: %s", err)
}
var newURNs []string
configuredURNs := d.Get("resources").(*schema.Set).List()
for _, rawConfiguredURN := range configuredURNs {
configuredURN := rawConfiguredURN.(string)
for _, apiURN := range *apiURNs {
if configuredURN == apiURN {
newURNs = append(newURNs, configuredURN)
}
}
}
if err = d.Set("resources", newURNs); err != nil {
return err
}
return nil
}
func resourceDigitalOceanProjectResourcesDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*CombinedConfig).godoClient()
projectId := d.Get("project").(string)
urns := d.Get("resources").(*schema.Set)
_, resp, err := client.Projects.Get(context.Background(), projectId)
if err != nil {
if resp != nil && resp.StatusCode == 404 {
// Project does not exist. Mark this resource as not existing.
d.SetId("")
return nil
}
return fmt.Errorf("Error while retrieving project: %s", err)
}
if urns.Len() > 0 {
if _, err = assignResourcesToDefaultProject(client, urns); err != nil {
return fmt.Errorf("Error assigning resources to default project: %s", err)
}
}
d.SetId("")
return nil
}

View File

@ -0,0 +1,130 @@
package digitalocean
import (
"context"
"fmt"
"strconv"
"testing"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
)
func TestAccDigitalOceanProjectResources_Basic(t *testing.T) {
projectName := generateProjectName()
dropletName := generateDropletName()
baseConfig := fmt.Sprintf(`
resource "digitalocean_project" "foo" {
name = "%s"
}
resource "digitalocean_droplet" "foobar" {
name = "%s"
size = "512mb"
image = "centos-7-x64"
region = "nyc3"
user_data = "foobar"
}
`, projectName, dropletName)
projectResourcesConfigEmpty := `
resource "digitalocean_project_resources" "barfoo" {
project = digitalocean_project.foo.id
resources = []
}
`
projectResourcesConfigWithDroplet := `
resource "digitalocean_project_resources" "barfoo" {
project = digitalocean_project.foo.id
resources = [digitalocean_droplet.foobar.urn]
}
`
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanProjectResourcesDestroy,
Steps: []resource.TestStep{
{
Config: baseConfig + projectResourcesConfigEmpty,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("digitalocean_project_resources.barfoo", "project"),
resource.TestCheckResourceAttr("digitalocean_project_resources.barfoo", "resources.#", "0"),
testProjectMembershipCount("digitalocean_project_resources.barfoo", 0),
),
},
{
// Add a resource to the digitalocean_project_resources.
Config: baseConfig + projectResourcesConfigWithDroplet,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("digitalocean_project_resources.barfoo", "project"),
resource.TestCheckResourceAttr("digitalocean_project_resources.barfoo", "resources.#", "1"),
testProjectMembershipCount("digitalocean_project_resources.barfoo", 1),
),
},
{
// Remove the resource that was added.
Config: baseConfig + projectResourcesConfigEmpty,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("digitalocean_project_resources.barfoo", "project"),
resource.TestCheckResourceAttr("digitalocean_project_resources.barfoo", "resources.#", "0"),
testProjectMembershipCount("digitalocean_project_resources.barfoo", 0),
),
},
},
})
}
func testProjectMembershipCount(name string, expectedCount int) resource.TestCheckFunc {
return testResourceInstanceState(name, func(is *terraform.InstanceState) error {
client := testAccProvider.Meta().(*CombinedConfig).godoClient()
projectId, ok := is.Attributes["project"]
if !ok {
return fmt.Errorf("project attribute not set")
}
resources, err := loadResourceURNs(client, projectId)
if err != nil {
return fmt.Errorf("Error retrieving project resources: %s", err)
}
actualCount := len(*resources)
if actualCount != expectedCount {
return fmt.Errorf("project membership count mismatch: expected=%d, actual=%d",
expectedCount, actualCount)
}
return nil
})
}
func testAccCheckDigitalOceanProjectResourcesDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*CombinedConfig).godoClient()
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "digitalocean_project":
_, _, err := client.Projects.Get(context.Background(), rs.Primary.ID)
if err == nil {
return fmt.Errorf("Project resource still exists")
}
case "digitalocean_droplet":
id, err := strconv.Atoi(rs.Primary.ID)
if err != nil {
return err
}
_, _, err = client.Droplets.Get(context.Background(), id)
if err == nil {
return fmt.Errorf("Droplet resource still exists")
}
}
}
return nil
}

View File

@ -139,6 +139,9 @@
<li<%= sidebar_current("docs-do-resource-project") %>>
<a href="/docs/providers/do/r/project.html">digitalocean_project</a>
</li>
<li<%= sidebar_current("docs-do-resource-project-resources") %>>
<a href="/docs/providers/do/r/project_resources.html">digitalocean_project_resources</a>
</li>
<li<%= sidebar_current("docs-do-resource-record") %>>
<a href="/docs/providers/do/r/record.html">digitalocean_record</a>
</li>

View File

@ -0,0 +1,61 @@
---
layout: "digitalocean"
page_title: "DigitalOcean: digitalocean_project_resources"
sidebar_current: "docs-do-resource-project-resources"
description: |-
Assign resources to a DigitalOcean Project.
---
# digitalocean\_project\_resources
Assign resources to a DigitalOcean Project. This is useful if you need to assign resources
managed in Terraform to a DigitalOcean Project managed outside of Terraform.
The following resource types can be associated with a project:
* Database Clusters
* Domains
* Droplets
* Floating IP
* Load Balancers
* Spaces Bucket
* Volume
## Example Usage
The following example assigns a droplet to a Project managed outside of Terraform:
```hcl
data "digitalocean_project" "playground" {
name = "playground"
}
resource "digitalocean_droplet" "foobar" {
name = "example"
size = "512mb"
image = "centos-7-x64"
region = "nyc3"
}
resource "digitalocean_project_resources" "barfoo" {
project = data.digitalocean_project.foo.id
resources = [
digitalocean_droplet.foobar.urn
]
}
```
## Argument Reference
The following arguments are supported:
* `project` - (Required) the ID of the project
* `resources` - (Required) a list of uniform resource names (URNs) for the resources associated with the project
## Attributes Reference
No additional attributes are exported.
## Import
Importing this resource is not supported.