From 2aad1f7bd2e1f2106d20d03164332b050d851719 Mon Sep 17 00:00:00 2001 From: Lars Wander Date: Thu, 20 Aug 2015 15:18:41 -0400 Subject: [PATCH] Implemented CRUD project metadata operations Common metadata state is now stored Optimistic locking support added to common_metadata Revisions to keys in project metadata are now reflected in the project state Wrote tests for project metadata (all pass) Relaxed test conditions to work on projects with extra keys Added documentation for project metadata --- builtin/providers/google/provider.go | 1 + .../resource_compute_project_metadata.go | 241 ++++++++++++++++++ .../resource_compute_project_metadata_test.go | 217 ++++++++++++++++ .../r/compute_project_metadata.html.markdown | 36 +++ website/source/layouts/google.erb | 4 + 5 files changed, 499 insertions(+) create mode 100644 builtin/providers/google/resource_compute_project_metadata.go create mode 100644 builtin/providers/google/resource_compute_project_metadata_test.go create mode 100644 website/source/docs/providers/google/r/compute_project_metadata.html.markdown diff --git a/builtin/providers/google/provider.go b/builtin/providers/google/provider.go index 690ec7e20..d7e293303 100644 --- a/builtin/providers/google/provider.go +++ b/builtin/providers/google/provider.go @@ -43,6 +43,7 @@ func Provider() terraform.ResourceProvider { "google_compute_instance": resourceComputeInstance(), "google_compute_instance_template": resourceComputeInstanceTemplate(), "google_compute_network": resourceComputeNetwork(), + "google_compute_project_metadata": resourceComputeProjectMetadata(), "google_compute_route": resourceComputeRoute(), "google_compute_target_pool": resourceComputeTargetPool(), "google_container_cluster": resourceContainerCluster(), diff --git a/builtin/providers/google/resource_compute_project_metadata.go b/builtin/providers/google/resource_compute_project_metadata.go new file mode 100644 index 000000000..ff0906dad --- /dev/null +++ b/builtin/providers/google/resource_compute_project_metadata.go @@ -0,0 +1,241 @@ +package google + +import ( + "fmt" + "log" + "time" + +// "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/compute/v1" +// "google.golang.org/api/googleapi" +) + +func resourceComputeProjectMetadata() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeProjectMetadataCreate, + Read: resourceComputeProjectMetadataRead, + Update: resourceComputeProjectMetadataUpdate, + Delete: resourceComputeProjectMetadataDelete, + + SchemaVersion: 0, + + Schema: map[string]*schema.Schema{ + "metadata": &schema.Schema { + Elem: schema.TypeString, + Type: schema.TypeMap, + Required: true, + }, + }, + } +} + +const FINGERPRINT_RETRIES = 10 +const FINGERPRINT_FAIL = "Invalid fingerprint." + +func resourceOperationWaitGlobal(config *Config, op *compute.Operation, activity string) error { + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for %s: %s", activity, err) + } + + op = opRaw.(*compute.Operation) + if op.Error != nil { + return OperationError(*op.Error) + } + + return nil +} + +func resourceComputeProjectMetadataCreate(d *schema.ResourceData, meta interface{}) error { + attempt := 0 + + config := meta.(*Config) + + for attempt < FINGERPRINT_RETRIES { + // Load project service + log.Printf("[DEBUG] Loading project service: %s", config.Project) + project, err := config.clientCompute.Projects.Get(config.Project).Do() + if err != nil { + return fmt.Errorf("Error loading project '%s': %s", config.Project, err) + } + + md := project.CommonInstanceMetadata + + newMDMap := d.Get("metadata").(map[string]interface{}) + // Ensure that we aren't overwriting entries that already exist + for _, kv := range(md.Items) { + if _, ok := newMDMap[kv.Key]; ok { + return fmt.Errorf("Error, key '%s' already exists in project '%s'", kv.Key, config.Project) + } + } + + // Append new metadata to existing metadata + for key, val := range(newMDMap) { + md.Items = append(md.Items, &compute.MetadataItems { + Key: key, + Value: val.(string), + }) + } + + op, err := config.clientCompute.Projects.SetCommonInstanceMetadata(config.Project, md).Do() + + if err != nil { + return fmt.Errorf("SetCommonInstanceMetadata failed: %s", err); + } + + log.Printf("[DEBUG] SetCommonMetadata: %d (%s)", op.Id, op.SelfLink) + + // Optimistic locking requires the fingerprint recieved to match + // the fingerprint we send the server, if there is a mismatch then we + // are working on old data, and must retry + err = resourceOperationWaitGlobal(config, op, "SetCommonMetadata") + if err == nil { + return resourceComputeProjectMetadataRead(d, meta) + } else if err.Error() == FINGERPRINT_FAIL { + attempt++ + } else { + return err + } + } + + return fmt.Errorf("Error, unable to set metadata resource after %d attempts", attempt) +} + +func resourceComputeProjectMetadataRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Load project service + log.Printf("[DEBUG] Loading project service: %s", config.Project) + project, err := config.clientCompute.Projects.Get(config.Project).Do() + if err != nil { + return fmt.Errorf("Error loading project '%s': %s", config.Project, err) + } + + md := project.CommonInstanceMetadata + + newMD := make(map[string]interface{}) + + for _, kv := range(md.Items) { + newMD[kv.Key] = kv.Value + } + + if err = d.Set("metadata", newMD); err != nil { + return fmt.Errorf("Error setting metadata: %s", err); + } + + d.SetId("common_metadata") + + return nil +} + +func resourceComputeProjectMetadataUpdate(d *schema.ResourceData, meta interface{}) error { + attempt := 0 + + config := meta.(*Config) + + if d.HasChange("metadata") { + o, n := d.GetChange("metadata") + oMDMap, nMDMap := o.(map[string]interface{}), n.(map[string]interface{}) + + for attempt < FINGERPRINT_RETRIES { + // Load project service + log.Printf("[DEBUG] Loading project service: %s", config.Project) + project, err := config.clientCompute.Projects.Get(config.Project).Do() + if err != nil { + return fmt.Errorf("Error loading project '%s': %s", config.Project, err) + } + + md := project.CommonInstanceMetadata + + curMDMap := make(map[string]string) + // Load metadata on server into map + for _, kv := range(md.Items) { + // If the server state has a key that we had in our old + // state, but not in our new state, we should delete it + _, okOld := oMDMap[kv.Key] + _, okNew := nMDMap[kv.Key] + if okOld && !okNew { + continue + } else { + curMDMap[kv.Key] = kv.Value + } + } + + // Insert new metadata into existing metadata (overwriting when needed) + for key, val := range(nMDMap) { + curMDMap[key] = val.(string) + } + + // Reformat old metadata into a list + md.Items = nil + for key, val := range(curMDMap) { + md.Items = append(md.Items, &compute.MetadataItems { + Key: key, + Value: val, + }) + } + + op, err := config.clientCompute.Projects.SetCommonInstanceMetadata(config.Project, md).Do() + + if err != nil { + return fmt.Errorf("SetCommonInstanceMetadata failed: %s", err); + } + + log.Printf("[DEBUG] SetCommonMetadata: %d (%s)", op.Id, op.SelfLink) + + // Optimistic locking requires the fingerprint recieved to match + // the fingerprint we send the server, if there is a mismatch then we + // are working on old data, and must retry + err = resourceOperationWaitGlobal(config, op, "SetCommonMetadata") + if err == nil { + return resourceComputeProjectMetadataRead(d, meta) + } else if err.Error() == FINGERPRINT_FAIL { + attempt++ + } else { + return err + } + } + + return fmt.Errorf("Error, unable to set metadata resource after %d attempts", attempt) + } + + return nil +} + +func resourceComputeProjectMetadataDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Load project service + log.Printf("[DEBUG] Loading project service: %s", config.Project) + project, err := config.clientCompute.Projects.Get(config.Project).Do() + if err != nil { + return fmt.Errorf("Error loading project '%s': %s", config.Project, err) + } + + md := project.CommonInstanceMetadata + + // Remove all items + md.Items = nil + + op, err := config.clientCompute.Projects.SetCommonInstanceMetadata(config.Project, md).Do() + + log.Printf("[DEBUG] SetCommonMetadata: %d (%s)", op.Id, op.SelfLink) + + err = resourceOperationWaitGlobal(config, op, "SetCommonMetadata") + if err != nil { + return err + } + + return resourceComputeProjectMetadataRead(d, meta) +} diff --git a/builtin/providers/google/resource_compute_project_metadata_test.go b/builtin/providers/google/resource_compute_project_metadata_test.go new file mode 100644 index 000000000..adcb25459 --- /dev/null +++ b/builtin/providers/google/resource_compute_project_metadata_test.go @@ -0,0 +1,217 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/compute/v1" +) + +// Add two key value pairs +func TestAccComputeProjectMetadata_basic(t *testing.T) { + var project compute.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeProjectMetadataDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeProject_basic0_metadata, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeProjectExists( + "google_compute_project_metadata.fizzbuzz", &project), + testAccCheckComputeProjectMetadataContains(&project, "banana", "orange"), + testAccCheckComputeProjectMetadataContains(&project, "sofa", "darwinism"), + testAccCheckComputeProjectMetadataSize(&project, 2), + ), + }, + }, + }) +} + +// Add three key value pairs, then replace one and modify a second +func TestAccComputeProjectMetadata_modify_1(t *testing.T) { + var project compute.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeProjectMetadataDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeProject_modify0_metadata, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeProjectExists( + "google_compute_project_metadata.fizzbuzz", &project), + testAccCheckComputeProjectMetadataContains(&project, "paper", "pen"), + testAccCheckComputeProjectMetadataContains(&project, "genghis_khan", "french bread"), + testAccCheckComputeProjectMetadataContains(&project, "happy", "smiling"), + testAccCheckComputeProjectMetadataSize(&project, 3), + ), + }, + + resource.TestStep{ + Config: testAccComputeProject_modify1_metadata, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeProjectExists( + "google_compute_project_metadata.fizzbuzz", &project), + testAccCheckComputeProjectMetadataContains(&project, "paper", "pen"), + testAccCheckComputeProjectMetadataContains(&project, "paris", "french bread"), + testAccCheckComputeProjectMetadataContains(&project, "happy", "laughing"), + testAccCheckComputeProjectMetadataSize(&project, 3), + ), + }, + }, + }) +} + +// Add two key value pairs, and replace both +func TestAccComputeProjectMetadata_modify_2(t *testing.T) { + var project compute.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeProjectMetadataDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeProject_basic0_metadata, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeProjectExists( + "google_compute_project_metadata.fizzbuzz", &project), + testAccCheckComputeProjectMetadataContains(&project, "banana", "orange"), + testAccCheckComputeProjectMetadataContains(&project, "sofa", "darwinism"), + testAccCheckComputeProjectMetadataSize(&project, 2), + ), + }, + + resource.TestStep{ + Config: testAccComputeProject_basic1_metadata, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeProjectExists( + "google_compute_project_metadata.fizzbuzz", &project), + testAccCheckComputeProjectMetadataContains(&project, "kiwi", "papaya"), + testAccCheckComputeProjectMetadataContains(&project, "finches", "darwinism"), + testAccCheckComputeProjectMetadataSize(&project, 2), + ), + }, + }, + }) +} + +func testAccCheckComputeProjectMetadataDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + project, err := config.clientCompute.Projects.Get(config.Project).Do() + if err == nil && len(project.CommonInstanceMetadata.Items) > 0 { + return fmt.Errorf("Error, metadata items still exist") + } + + return nil +} + +func testAccCheckComputeProjectExists(n string, project *compute.Project) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientCompute.Projects.Get( + config.Project).Do() + if err != nil { + return err + } + + if "common_metadata" != rs.Primary.ID { + return fmt.Errorf("Common metadata not found, found %s", rs.Primary.ID) + } + + *project = *found + + return nil + } +} + +func testAccCheckComputeProjectMetadataContains(project *compute.Project, key string, 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 fmt.Errorf("Error, failed to load project service for %s: %s", config.Project, err) + } + + for _, kv := range(project.CommonInstanceMetadata.Items) { + if kv.Key == key { + if (kv.Value == value) { + return nil + } else { + return fmt.Errorf("Error, key value mismatch, wanted (%s, %s), got (%s, %s)", + key, value, kv.Key, kv.Value); + } + } + } + + return fmt.Errorf("Error, key %s not present", key) + } +} + +func testAccCheckComputeProjectMetadataSize(project *compute.Project, size int) 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 fmt.Errorf("Error, failed to load project service for %s: %s", config.Project, err) + } + + if size > len(project.CommonInstanceMetadata.Items) { + return fmt.Errorf("Error, expected at least %d metadata items, got %d", size, + len(project.CommonInstanceMetadata.Items)) + } + + return nil + } +} + +const testAccComputeProject_basic0_metadata = ` +resource "google_compute_project_metadata" "fizzbuzz" { + metadata { + banana = "orange" + sofa = "darwinism" + } +}` + +const testAccComputeProject_basic1_metadata = ` +resource "google_compute_project_metadata" "fizzbuzz" { + metadata { + kiwi = "papaya" + finches = "darwinism" + } +}` + +const testAccComputeProject_modify0_metadata = ` +resource "google_compute_project_metadata" "fizzbuzz" { + metadata { + paper = "pen" + genghis_khan = "french bread" + happy = "smiling" + } +}` + +const testAccComputeProject_modify1_metadata = ` +resource "google_compute_project_metadata" "fizzbuzz" { + metadata { + paper = "pen" + paris = "french bread" + happy = "laughing" + } +}` diff --git a/website/source/docs/providers/google/r/compute_project_metadata.html.markdown b/website/source/docs/providers/google/r/compute_project_metadata.html.markdown new file mode 100644 index 000000000..aedb201a0 --- /dev/null +++ b/website/source/docs/providers/google/r/compute_project_metadata.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "google" +page_title: "Google: google_compute_project_metadata" +sidebar_current: "docs-google-resource-project-metadata" +description: |- + Manages common instance metadata +--- + +# google\_compute\_project\_metadata + +Manages metadata common to all instances for a project in GCE. + +## Example Usage + +``` +resource "google_compute_project_metadata" "default" { + metadata { + foo = "bar" + fizz = "buzz" + 13 = "42" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `metadata` - (Required) A series of key value pairs. Changing this resource updates + the GCE state. + +## Attributes Reference + +The following attributes are exported: + +* `metadata` - Common instance metadata. diff --git a/website/source/layouts/google.erb b/website/source/layouts/google.erb index 320e4f83a..b14669168 100644 --- a/website/source/layouts/google.erb +++ b/website/source/layouts/google.erb @@ -53,6 +53,10 @@ google_compute_network + > + google_compute_project_metadata + + > google_compute_route