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:
parent
083c9cbfcd
commit
3a3c374c3f
|
@ -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(),
|
||||
|
|
|
@ -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},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue