provider/google: Add support for Google Compute Stogare buckets.

Configure Google Compute Storage buckets using:
* name (compulsory attribute)
* predefined_acl (optional, default: `projectPrivate`)
* location (optional, default: `US`)
* force_destroy (optional, default: `false`)

Currently supporting only `predefined_acl`s. Bucket attribute updates happen via re-creation. force_destroy will cause bucket objects to be purged, enabling bucket destruction.
This commit is contained in:
Dan Hilton 2015-05-21 18:28:27 +01:00 committed by Michal Tekel
parent e5f5e1a167
commit b909f7826b
6 changed files with 422 additions and 1 deletions

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/oauth2/jwt"
"google.golang.org/api/compute/v1"
"google.golang.org/api/dns/v1"
"google.golang.org/api/storage/v1"
)
// Config is the configuration structure used to instantiate the Google
@ -25,7 +26,8 @@ type Config struct {
Region string
clientCompute *compute.Service
clientDns *dns.Service
clientDns *dns.Service
clientStorage *storage.Service
}
func (c *Config) loadAndValidate() error {
@ -55,6 +57,7 @@ func (c *Config) loadAndValidate() error {
clientScopes := []string{
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/ndev.clouddns.readwrite",
"https://www.googleapis.com/auth/devstorage.full_control",
}
// Get the token for use in our requests
@ -114,6 +117,13 @@ func (c *Config) loadAndValidate() error {
}
c.clientDns.UserAgent = userAgent
log.Printf("[INFO] Instantiating Google Storage Client...")
c.clientStorage, err = storage.New(client)
if err != nil {
return err
}
c.clientStorage.UserAgent = userAgent
return nil
}

View File

@ -41,6 +41,7 @@ func Provider() terraform.ResourceProvider {
"google_compute_target_pool": resourceComputeTargetPool(),
"google_dns_managed_zone": resourceDnsManagedZone(),
"google_dns_record_set": resourceDnsRecordSet(),
"google_storage_bucket": resourceStorageBucket(),
},
ConfigureFunc: providerConfigure,

View File

@ -0,0 +1,144 @@
package google
import (
"errors"
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/storage/v1"
)
func resourceStorageBucket() *schema.Resource {
return &schema.Resource{
Create: resourceStorageBucketCreate,
Read: resourceStorageBucketRead,
Update: resourceStorageBucketUpdate,
Delete: resourceStorageBucketDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"predefined_acl": &schema.Schema{
Type: schema.TypeString,
Default: "projectPrivate",
Optional: true,
ForceNew: true,
},
"location": &schema.Schema{
Type: schema.TypeString,
Default: "US",
Optional: true,
ForceNew: true,
},
"force_destroy": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
}
}
func resourceStorageBucketCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
// Get the bucket and acl
bucket := d.Get("name").(string)
acl := d.Get("predefined_acl").(string)
location := d.Get("location").(string)
// Create a bucket, setting the acl, location and name.
sb := &storage.Bucket{Name: bucket, Location: location}
res, err := config.clientStorage.Buckets.Insert(config.Project, sb).PredefinedAcl(acl).Do()
if err != nil {
fmt.Printf("Error creating bucket %s: %v", bucket, err)
return err
}
log.Printf("[DEBUG] Created bucket %v at location %v\n\n", res.Name, res.SelfLink)
// Assign the bucket ID as the resource ID
d.SetId(res.Id)
return nil
}
func resourceStorageBucketUpdate(d *schema.ResourceData, meta interface{}) error {
// Only thing you can currently change is force_delete (all other properties have ForceNew)
// which is just terraform object state change, so nothing to do here
return nil
}
func resourceStorageBucketRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
// Get the bucket and acl
bucket := d.Get("name").(string)
res, err := config.clientStorage.Buckets.Get(bucket).Do()
if err != nil {
fmt.Printf("Error reading bucket %s: %v", bucket, err)
return err
}
log.Printf("[DEBUG] Read bucket %v at location %v\n\n", res.Name, res.SelfLink)
// Update the bucket ID according to the resource ID
d.SetId(res.Id)
return nil
}
func resourceStorageBucketDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
// Get the bucket
bucket := d.Get("name").(string)
for {
res, err := config.clientStorage.Objects.List(bucket).Do()
if err != nil {
fmt.Printf("Error Objects.List failed: %v", err)
return err
}
if len(res.Items) != 0 {
if d.Get("force_destroy").(bool) {
// purge the bucket...
log.Printf("[DEBUG] GCS Bucket attempting to forceDestroy\n\n")
for _, object := range res.Items {
log.Printf("[DEBUG] Found %s", object.Name)
if err := config.clientStorage.Objects.Delete(bucket, object.Name).Do(); err != nil {
log.Fatalf("Error trying to delete object: %s %s\n\n", object.Name, err)
} else {
log.Printf("Object deleted: %s \n\n", object.Name)
}
}
} else {
delete_err := errors.New("Error trying to delete a bucket containing objects without `force_destroy` set to true")
log.Printf("Error! %s : %s\n\n", bucket, delete_err)
return delete_err
}
} else {
break // 0 items, bucket empty
}
}
// remove empty bucket
err := config.clientStorage.Buckets.Delete(bucket).Do()
if err != nil {
fmt.Printf("Error deleting bucket %s: %v\n\n", bucket, err)
return err
}
log.Printf("[DEBUG] Deleted bucket %v\n\n", bucket)
return nil
}

View File

@ -0,0 +1,231 @@
package google
import (
"fmt"
"math/rand"
"bytes"
"testing"
"time"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"google.golang.org/api/googleapi"
storage "google.golang.org/api/storage/v1"
)
func TestAccStorageDefaults(t *testing.T) {
var bucketName string
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccGoogleStorageDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testGoogleStorageBucketsReaderDefaults,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStorageBucketExists(
"google_storage_bucket.bucket", &bucketName),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "predefined_acl", "projectPrivate"),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "location", "US"),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "force_destroy", "false"),
),
},
},
})
}
func TestAccStorageCustomAttributes(t *testing.T) {
var bucketName string
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccGoogleStorageDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testGoogleStorageBucketsReaderCustomAttributes,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStorageBucketExists(
"google_storage_bucket.bucket", &bucketName),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "predefined_acl", "publicReadWrite"),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "location", "EU"),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "force_destroy", "true"),
),
},
},
})
}
func TestAccStorageBucketUpdate(t *testing.T) {
var bucketName string
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccGoogleStorageDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testGoogleStorageBucketsReaderDefaults,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStorageBucketExists(
"google_storage_bucket.bucket", &bucketName),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "predefined_acl", "projectPrivate"),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "location", "US"),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "force_destroy", "false"),
),
},
resource.TestStep{
Config: testGoogleStorageBucketsReaderCustomAttributes,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStorageBucketExists(
"google_storage_bucket.bucket", &bucketName),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "predefined_acl", "publicReadWrite"),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "location", "EU"),
resource.TestCheckResourceAttr(
"google_storage_bucket.bucket", "force_destroy", "true"),
),
},
},
})
}
func TestAccStorageForceDestroy(t *testing.T) {
var bucketName string
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccGoogleStorageDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testGoogleStorageBucketsReaderCustomAttributes,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStorageBucketExists(
"google_storage_bucket.bucket", &bucketName),
),
},
resource.TestStep{
Config: testGoogleStorageBucketsReaderCustomAttributes,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStorageBucketPutItem(&bucketName),
),
},
resource.TestStep{
Config: "",
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStorageBucketMissing(&bucketName),
),
},
},
})
}
func testAccCheckCloudStorageBucketExists(n string, bucketName *string) 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 Project_ID is set")
}
config := testAccProvider.Meta().(*Config)
found, err := config.clientStorage.Buckets.Get(rs.Primary.ID).Do()
if err != nil {
return err
}
if found.Id != rs.Primary.ID {
return fmt.Errorf("Bucket not found")
}
*bucketName = found.Name
return nil
}
}
func testAccCheckCloudStorageBucketPutItem(bucketName *string) resource.TestCheckFunc {
return func(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
data := bytes.NewBufferString("test")
dataReader := bytes.NewReader(data.Bytes())
object := &storage.Object{Name: "bucketDestroyTestFile"}
// This needs to use Media(io.Reader) call, otherwise it does not go to /upload API and fails
if res, err := config.clientStorage.Objects.Insert(*bucketName, object).Media(dataReader).Do(); err == nil {
fmt.Printf("Created object %v at location %v\n\n", res.Name, res.SelfLink)
} else {
return fmt.Errorf("Objects.Insert failed: %v", err)
}
return nil
}
}
func testAccCheckCloudStorageBucketMissing(bucketName *string) resource.TestCheckFunc {
return func(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
_, err := config.clientStorage.Buckets.Get(*bucketName).Do()
if err == nil {
return fmt.Errorf("Found %s", *bucketName)
}
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 {
return nil
} else {
return err
}
}
}
func testAccGoogleStorageDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
for _, rs := range s.RootModule().Resources {
if rs.Type != "google_storage_bucket" {
continue
}
_, err := config.clientStorage.Buckets.Get(rs.Primary.ID).Do()
if err == nil {
return fmt.Errorf("Bucket still exists")
}
}
return nil
}
var randInt = rand.New(rand.NewSource(time.Now().UnixNano())).Int()
var testGoogleStorageBucketsReaderDefaults = fmt.Sprintf(`
resource "google_storage_bucket" "bucket" {
name = "tf-test-bucket-%d"
}
`, randInt)
var testGoogleStorageBucketsReaderCustomAttributes = fmt.Sprintf(`
resource "google_storage_bucket" "bucket" {
name = "tf-test-bucket-%d"
predefined_acl = "publicReadWrite"
location = "EU"
force_destroy = "true"
}
`, randInt)

View File

@ -0,0 +1,32 @@
---
layout: "google"
page_title: "Google: google_storage_bucket"
sidebar_current: "docs-google-resource-storage"
description: |-
Creates a new bucket in Google Cloud Storage.
---
# google\_storage\_bucket
Creates a new bucket in Google cloud storage service(GCS). Currently, it will not change location nor ACL once a bucket has been created with Terraform. For more information see [the official documentation](https://cloud.google.com/storage/docs/overview) and [API](https://cloud.google.com/storage/docs/json_api).
## Example Usage
Example creating a private bucket in standard storage, in the EU region.
```
resource "google_storage_bucket" "image-store" {
name = "image-store-bucket"
predefined_acl = "projectPrivate"
location = "EU"
}
```
## Argument Reference
* `name` - (Required) The name of the bucket.
* `predefined_acl` - (Optional, Default: 'private') The [canned GCS ACL](https://cloud.google.com/storage/docs/access-control#predefined-acl) to apply.
* `location` - (Optional, Default: 'US') The [GCS location](https://cloud.google.com/storage/docs/bucket-locations)
* `force_destroy` - (Optional, Default: false) When deleting a bucket, this boolean option will delete all contained objects. If you try to delete a bucket that contains objects, Terraform will fail that run.

View File

@ -60,6 +60,9 @@
<li<%= sidebar_current("docs-google-resource-dns-record-set") %>>
<a href="/docs/providers/google/r/dns_record_set.html">google_dns_record_set</a>
</li>
<li<%= sidebar_current("docs-google-resource-storage-bucket") %>>
<a href="/docs/providers/google/r/storage_bucket.html">google_storage_bucket</a>
</li>
</ul>
</li>
</ul>