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
This commit is contained in:
Lars Wander 2015-08-20 15:18:41 -04:00
parent a535f9eda1
commit 2aad1f7bd2
5 changed files with 499 additions and 0 deletions

View File

@ -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(),

View File

@ -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)
}

View File

@ -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"
}
}`

View File

@ -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.

View File

@ -53,6 +53,10 @@
<a href="/docs/providers/google/r/compute_network.html">google_compute_network</a>
</li>
<li<%= sidebar_current("docs-google-resource-project-metadata") %>>
<a href="/docs/providers/google/r/compute_project_metadata.html">google_compute_project_metadata</a>
</li>
<li<%= sidebar_current("docs-google-resource-route") %>>
<a href="/docs/providers/google/r/compute_route.html">google_compute_route</a>
</li>