diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a3fce7..4844328f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/google/import_compute_project_metadata_item_test.go b/google/import_compute_project_metadata_item_test.go new file mode 100644 index 00000000..8b0089b8 --- /dev/null +++ b/google/import_compute_project_metadata_item_test.go @@ -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, + }, + }, + }) +} diff --git a/google/metadata.go b/google/metadata.go index e2ebd18a..7e20e58f 100644 --- a/google/metadata.go +++ b/google/metadata.go @@ -2,6 +2,7 @@ package google import ( "fmt" + "log" "google.golang.org/api/compute/v1" ) @@ -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 +} diff --git a/google/provider.go b/google/provider.go index 5bff44c8..bda33019 100644 --- a/google/provider.go +++ b/google/provider.go @@ -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(), diff --git a/google/resource_compute_project_metadata_item.go b/google/resource_compute_project_metadata_item.go new file mode 100644 index 00000000..dc017d32 --- /dev/null +++ b/google/resource_compute_project_metadata_item.go @@ -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) +} diff --git a/google/resource_compute_project_metadata_item_test.go b/google/resource_compute_project_metadata_item_test.go new file mode 100644 index 00000000..695ab2e1 --- /dev/null +++ b/google/resource_compute_project_metadata_item_test.go @@ -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) +} diff --git a/website/docs/r/compute_project_metadata.html.markdown b/website/docs/r/compute_project_metadata.html.markdown index 3b7dd19d..c240fb7c 100644 --- a/website/docs/r/compute_project_metadata.html.markdown +++ b/website/docs/r/compute_project_metadata.html.markdown @@ -8,7 +8,10 @@ description: |- # google\_compute\_project\_metadata -Manages metadata common to all instances for a project in GCE. +Manages metadata common to all instances for a project in GCE. If you +want to manage only single key/value pairs within the project metadata +rather than the entire set, then use +[google_compute_project_metadata_item](compute_project_metadata_item.html). ## Example Usage diff --git a/website/docs/r/compute_project_metadata_item.html.markdown b/website/docs/r/compute_project_metadata_item.html.markdown new file mode 100644 index 00000000..a48dd7e2 --- /dev/null +++ b/website/docs/r/compute_project_metadata_item.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "google" +page_title: "Google: google_compute_project_metadata_item" +sidebar_current: "docs-google-compute-project-metadata-item" +description: |- + Manages a single key/value pair on common instance metadata +--- + +# google\_compute\_project\_metadata\_item + +Manages a single key/value pair on metadata common to all instances for +a project in GCE. Using `google_compute_project_metadata_item` lets you +manage a single key/value setting in Terraform rather than the entire +project metadata map. + +## Example Usage + +```hcl +resource "google_compute_project_metadata_item" "default" { + key = "my_metadata" + value = "my_value" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `key` - (Required) The metadata key to set. + +* `value` - (Required) The value to set for the given metadata key. + +- - - + +* `project` - (Optional) The project in which the resource belongs. If it + is not provided, the provider project is used. + +## Attributes Reference + +Only the arguments listed above are exposed as attributes. + +## Import + +Project metadata items can be imported using the `key`, e.g. + +``` +$ terraform import google_compute_project_metadata_item.default my_metadata +``` diff --git a/website/google.erb b/website/google.erb index 42ad6aee..2375c65f 100644 --- a/website/google.erb +++ b/website/google.erb @@ -153,6 +153,10 @@ google_compute_project_metadata + > + google_compute_project_metadata_item + + > google_compute_region_backend_service