diff --git a/CHANGELOG.md b/CHANGELOG.md index e752f1f2..44000bb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ BACKWARDS INCOMPATIBILITIES / NOTES: FEATURES: +* **New Resource:** `google_compute_project_metadata_item` - allows management of single key/value pairs within the project metadata map [GH-176] IMPROVEMENTS: * **New Resource:** `google_compute_network_peering` ([#259](https://github.com/terraform-providers/terraform-provider-google/issues/259)) 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..230b7a16 100644 --- a/google/metadata.go +++ b/google/metadata.go @@ -2,12 +2,15 @@ package google import ( "fmt" + "log" + "strings" "google.golang.org/api/compute/v1" ) const FINGERPRINT_RETRIES = 10 -const FINGERPRINT_FAIL = "Invalid fingerprint." + +var FINGERPRINT_FAIL_ERRORS = []string{"Invalid fingerprint.", "Supplied fingerprint does not match current metadata fingerprint."} // Since the google compute API uses optimistic locking, there is a chance // we need to resubmit our updated metadata. To do this, you need to provide @@ -16,11 +19,25 @@ func MetadataRetryWrapper(update func() error) error { attempt := 0 for attempt < FINGERPRINT_RETRIES { err := update() - if err != nil && err.Error() == FINGERPRINT_FAIL { - attempt++ - } else { + if err == nil { + return nil + } + + // Check to see if the error matches any of our fingerprint-related failure messages + var fingerprintError bool + for _, msg := range FINGERPRINT_FAIL_ERRORS { + if strings.Contains(err.Error(), msg) { + fingerprintError = true + break + } + } + + if !fingerprintError { + // Something else went wrong, don't retry return err } + + attempt++ } return fmt.Errorf("Failed to update metadata after %d retries", attempt) @@ -71,3 +88,43 @@ 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 { + // Make a copy of value as we need a ptr type; if we directly use 'value' then all items will reference the same + // memory address + vtmp := value + metadata[idx] = &compute.MetadataItems{Key: key, Value: &vtmp} + idx++ + } + + return metadata +} diff --git a/google/provider.go b/google/provider.go index bfec54d9..00a8c829 100644 --- a/google/provider.go +++ b/google/provider.go @@ -88,6 +88,7 @@ func Provider() terraform.ResourceProvider { "google_compute_network": resourceComputeNetwork(), "google_compute_network_peering": resourceComputeNetworkPeering(), "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..c6539542 --- /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 computeOperationWait(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