From b909f7826b3e8c171e1ee295584394541c5bff75 Mon Sep 17 00:00:00 2001 From: Dan Hilton Date: Thu, 21 May 2015 18:28:27 +0100 Subject: [PATCH] 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. --- builtin/providers/google/config.go | 12 +- builtin/providers/google/provider.go | 1 + .../google/resource_storage_bucket.go | 144 +++++++++++ .../google/resource_storage_bucket_test.go | 231 ++++++++++++++++++ .../google/r/storage_bucket.html.markdown | 32 +++ website/source/layouts/google.erb | 3 + 6 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 builtin/providers/google/resource_storage_bucket.go create mode 100644 builtin/providers/google/resource_storage_bucket_test.go create mode 100644 website/source/docs/providers/google/r/storage_bucket.html.markdown diff --git a/builtin/providers/google/config.go b/builtin/providers/google/config.go index de97df603..25348bbf2 100644 --- a/builtin/providers/google/config.go +++ b/builtin/providers/google/config.go @@ -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 } diff --git a/builtin/providers/google/provider.go b/builtin/providers/google/provider.go index 09687a770..1554d9154 100644 --- a/builtin/providers/google/provider.go +++ b/builtin/providers/google/provider.go @@ -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, diff --git a/builtin/providers/google/resource_storage_bucket.go b/builtin/providers/google/resource_storage_bucket.go new file mode 100644 index 000000000..59370720e --- /dev/null +++ b/builtin/providers/google/resource_storage_bucket.go @@ -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 +} diff --git a/builtin/providers/google/resource_storage_bucket_test.go b/builtin/providers/google/resource_storage_bucket_test.go new file mode 100644 index 000000000..e33cd5cb7 --- /dev/null +++ b/builtin/providers/google/resource_storage_bucket_test.go @@ -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) diff --git a/website/source/docs/providers/google/r/storage_bucket.html.markdown b/website/source/docs/providers/google/r/storage_bucket.html.markdown new file mode 100644 index 000000000..b5aa15353 --- /dev/null +++ b/website/source/docs/providers/google/r/storage_bucket.html.markdown @@ -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. diff --git a/website/source/layouts/google.erb b/website/source/layouts/google.erb index fc36ea215..1cca724da 100644 --- a/website/source/layouts/google.erb +++ b/website/source/layouts/google.erb @@ -60,6 +60,9 @@ > google_dns_record_set + > + google_storage_bucket +