Skip to content

Commit

Permalink
Add support for google_compute_project_metadata_item (hashicorp#176)
Browse files Browse the repository at this point in the history
* Add support for google_compute_project_metadata_item

This allows terraform users to manage single key/value items within the
project metadata map, rather than the entire map itself.

* Update CHANGELOG.md

* Add details about import
  • Loading branch information
selmanj authored Jul 17, 2017
1 parent 5628c67 commit 236c0f5
Show file tree
Hide file tree
Showing 9 changed files with 417 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ FEATURES:

* **New Resource:** `google_bigtable_instance` [GH-177]
* **New Resource:** `google_bigtable_table` [GH-177]
* **New Resource:** `google_compute_project_metadata_item` - allows management of single key/value pairs within the project metadata map [GH-176]

IMPROVEMENTS:

Expand Down
25 changes: 25 additions & 0 deletions google/import_compute_project_metadata_item_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package google

import (
"testing"

"github.com/hashicorp/terraform/helper/resource"
)

func TestAccComputeProjectMetadataItem_importBasic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckProjectMetadataItemDestroy,
Steps: []resource.TestStep{
{
Config: testAccProjectMetadataItem_basic("myKey", "myValue"),
},
{
ResourceName: "google_compute_project_metadata_item.foobar",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
38 changes: 38 additions & 0 deletions google/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package google

import (
"fmt"
"log"

"google.golang.org/api/compute/v1"
)
Expand Down Expand Up @@ -71,3 +72,40 @@ func MetadataFormatSchema(curMDMap map[string]interface{}, md *compute.Metadata)

return newMD
}

// flattenComputeMetadata transforms a list of MetadataItems (as returned via the GCP client) into a simple map from key
// to value.
func flattenComputeMetadata(metadata []*compute.MetadataItems) map[string]string {
m := map[string]string{}

for _, item := range metadata {
// check for duplicates
if item.Value == nil {
continue
}
if val, ok := m[item.Key]; ok {
// warn loudly!
log.Printf("[WARN] Key '%s' already has value '%s' when flattening - ignoring incoming value '%s'",
item.Key,
val,
*item.Value)
}
m[item.Key] = *item.Value
}

return m
}

// expandComputeMetadata transforms a map representing computing metadata into a list of compute.MetadataItems suitable
// for the GCP client.
func expandComputeMetadata(m map[string]string) []*compute.MetadataItems {
metadata := make([]*compute.MetadataItems, len(m))

idx := 0
for key, value := range m {
metadata[idx] = &compute.MetadataItems{Key: key, Value: &value}
idx++
}

return metadata
}
1 change: 1 addition & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func Provider() terraform.ResourceProvider {
"google_compute_instance_template": resourceComputeInstanceTemplate(),
"google_compute_network": resourceComputeNetwork(),
"google_compute_project_metadata": resourceComputeProjectMetadata(),
"google_compute_project_metadata_item": resourceComputeProjectMetadataItem(),
"google_compute_region_backend_service": resourceComputeRegionBackendService(),
"google_compute_route": resourceComputeRoute(),
"google_compute_router": resourceComputeRouter(),
Expand Down
178 changes: 178 additions & 0 deletions google/resource_compute_project_metadata_item.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package google

import (
"fmt"
"log"

"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
)

func resourceComputeProjectMetadataItem() *schema.Resource {
return &schema.Resource{
Create: resourceComputeProjectMetadataItemCreate,
Read: resourceComputeProjectMetadataItemRead,
Update: resourceComputeProjectMetadataItemUpdate,
Delete: resourceComputeProjectMetadataItemDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"key": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"value": {
Type: schema.TypeString,
Required: true,
},
"project": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
}
}

func resourceComputeProjectMetadataItemCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

projectID, err := getProject(d, config)
if err != nil {
return err
}

key := d.Get("key").(string)
val := d.Get("value").(string)

err = updateComputeCommonInstanceMetadata(config, projectID, key, &val)
if err != nil {
return err
}

d.SetId(key)

return nil
}

func resourceComputeProjectMetadataItemRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

projectID, err := getProject(d, config)
if err != nil {
return err
}

log.Printf("[DEBUG] Loading project metadata: %s", projectID)
project, err := config.clientCompute.Projects.Get(projectID).Do()
if err != nil {
return fmt.Errorf("Error loading project '%s': %s", projectID, err)
}

md := flattenComputeMetadata(project.CommonInstanceMetadata.Items)
val, ok := md[d.Id()]
if !ok {
// Resource no longer exists
d.SetId("")
return nil
}

d.Set("key", d.Id())
d.Set("value", val)

return nil
}

func resourceComputeProjectMetadataItemUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

projectID, err := getProject(d, config)
if err != nil {
return err
}

if d.HasChange("value") {
key := d.Get("key").(string)
_, n := d.GetChange("value")
new := n.(string)

err = updateComputeCommonInstanceMetadata(config, projectID, key, &new)
if err != nil {
return err
}
}
return nil
}

func resourceComputeProjectMetadataItemDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

projectID, err := getProject(d, config)
if err != nil {
return err
}

key := d.Get("key").(string)

err = updateComputeCommonInstanceMetadata(config, projectID, key, nil)
if err != nil {
return err
}

d.SetId("")
return nil
}

func updateComputeCommonInstanceMetadata(config *Config, projectID string, key string, afterVal *string) error {
updateMD := func() error {
log.Printf("[DEBUG] Loading project metadata: %s", projectID)
project, err := config.clientCompute.Projects.Get(projectID).Do()
if err != nil {
return fmt.Errorf("Error loading project '%s': %s", projectID, err)
}

md := flattenComputeMetadata(project.CommonInstanceMetadata.Items)

val, ok := md[key]

if !ok {
if afterVal == nil {
// Asked to set no value and we didn't find one - we're done
return nil
}
} else {
if afterVal != nil && *afterVal == val {
// Asked to set a value and it's already set - we're done.
return nil
}
}

if afterVal == nil {
delete(md, key)
} else {
md[key] = *afterVal
}

// Attempt to write the new value now
op, err := config.clientCompute.Projects.SetCommonInstanceMetadata(
projectID,
&compute.Metadata{
Fingerprint: project.CommonInstanceMetadata.Fingerprint,
Items: expandComputeMetadata(md),
},
).Do()

if err != nil {
return err
}

log.Printf("[DEBUG] SetCommonInstanceMetadata: %d (%s)", op.Id, op.SelfLink)

return computeOperationWaitGlobal(config, op, project.Name, "SetCommonInstanceMetadata")
}

return MetadataRetryWrapper(updateMD)
}
118 changes: 118 additions & 0 deletions google/resource_compute_project_metadata_item_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package google

import (
"fmt"
"testing"

"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestAccComputeProjectMetadataItem_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckProjectMetadataItemDestroy,
Steps: []resource.TestStep{
{
Config: testAccProjectMetadataItem_basic("myKey", "myValue"),
Check: resource.ComposeTestCheckFunc(
testAccCheckProjectMetadataItem_hasMetadata("myKey", "myValue"),
),
},
},
})
}

func TestAccComputeProjectMetadataItem_basicWithEmptyVal(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckProjectMetadataItemDestroy,
Steps: []resource.TestStep{
{
Config: testAccProjectMetadataItem_basic("myKey", ""),
Check: resource.ComposeTestCheckFunc(
testAccCheckProjectMetadataItem_hasMetadata("myKey", ""),
),
},
},
})
}

func TestAccComputeProjectMetadataItem_basicUpdate(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckProjectMetadataItemDestroy,
Steps: []resource.TestStep{
{
Config: testAccProjectMetadataItem_basic("myKey", "myValue"),
Check: resource.ComposeTestCheckFunc(
testAccCheckProjectMetadataItem_hasMetadata("myKey", "myValue"),
),
},
{
Config: testAccProjectMetadataItem_basic("myKey", "myUpdatedValue"),
Check: resource.ComposeTestCheckFunc(
testAccCheckProjectMetadataItem_hasMetadata("myKey", "myUpdatedValue"),
),
},
},
})
}

func testAccCheckProjectMetadataItem_hasMetadata(key, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)

project, err := config.clientCompute.Projects.Get(config.Project).Do()
if err != nil {
return err
}

metadata := flattenComputeMetadata(project.CommonInstanceMetadata.Items)

val, ok := metadata[key]
if !ok {
return fmt.Errorf("Unable to find a value for key '%s'", key)
}
if val != value {
return fmt.Errorf("Value for key '%s' does not match. Expected '%s' but found '%s'", key, value, val)
}
return nil
}
}

func testAccCheckProjectMetadataItemDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)

project, err := config.clientCompute.Projects.Get(config.Project).Do()
if err != nil {
return err
}

metadata := flattenComputeMetadata(project.CommonInstanceMetadata.Items)

for _, rs := range s.RootModule().Resources {
if rs.Type != "google_compute_project_metadata_item" {
continue
}

_, ok := metadata[rs.Primary.ID]
if ok {
return fmt.Errorf("Metadata key/value '%s': '%s' still exist", rs.Primary.Attributes["key"], rs.Primary.Attributes["value"])
}
}

return nil
}

func testAccProjectMetadataItem_basic(key, val string) string {
return fmt.Sprintf(`
resource "google_compute_project_metadata_item" "foobar" {
key = "%s"
value = "%s"
}
`, key, val)
}
Loading

0 comments on commit 236c0f5

Please sign in to comment.