Merge pull request #2060 from alphagov/f-add-google-cloud-storage
Feature: add google cloud storage
This commit is contained in:
commit
265b9b254e
|
@ -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
|
||||
|
@ -26,6 +27,7 @@ type Config struct {
|
|||
|
||||
clientCompute *compute.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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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.
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue