Merge pull request #3065 from lwander/gce-project-metadata

GCE project metadata
This commit is contained in:
Dave Cunningham 2015-08-24 13:35:50 -04:00
commit 06c7ff4864
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": resourceComputeInstance(),
"google_compute_instance_template": resourceComputeInstanceTemplate(), "google_compute_instance_template": resourceComputeInstanceTemplate(),
"google_compute_network": resourceComputeNetwork(), "google_compute_network": resourceComputeNetwork(),
"google_compute_project_metadata": resourceComputeProjectMetadata(),
"google_compute_route": resourceComputeRoute(), "google_compute_route": resourceComputeRoute(),
"google_compute_target_pool": resourceComputeTargetPool(), "google_compute_target_pool": resourceComputeTargetPool(),
"google_container_cluster": resourceContainerCluster(), "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> <a href="/docs/providers/google/r/compute_network.html">google_compute_network</a>
</li> </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") %>> <li<%= sidebar_current("docs-google-resource-route") %>>
<a href="/docs/providers/google/r/compute_route.html">google_compute_route</a> <a href="/docs/providers/google/r/compute_route.html">google_compute_route</a>
</li> </li>