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