diff --git a/google/provider.go b/google/provider.go index 1d3cca5ea0f..103058dcf77 100644 --- a/google/provider.go +++ b/google/provider.go @@ -190,6 +190,7 @@ func Provider() terraform.ResourceProvider { "google_storage_bucket_iam_member": ResourceIamMember(IamStorageBucketSchema, NewStorageBucketIamUpdater), "google_storage_bucket_object": resourceStorageBucketObject(), "google_storage_object_acl": resourceStorageObjectAcl(), + "google_storage_default_object_acl": resourceStorageDefaultObjectAcl(), }, ConfigureFunc: providerConfigure, diff --git a/google/resource_storage_default_object_acl.go b/google/resource_storage_default_object_acl.go new file mode 100644 index 00000000000..1dda438953f --- /dev/null +++ b/google/resource_storage_default_object_acl.go @@ -0,0 +1,199 @@ +package google + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/storage/v1" +) + +func resourceStorageDefaultObjectAcl() *schema.Resource { + return &schema.Resource{ + Create: resourceStorageDefaultObjectAclCreate, + Read: resourceStorageDefaultObjectAclRead, + Update: resourceStorageDefaultObjectAclUpdate, + Delete: resourceStorageDefaultObjectAclDelete, + + Schema: map[string]*schema.Schema{ + "bucket": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "role_entity": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func getDefaultObjectAclId(bucket string) string { + return bucket + "-default-object-acl" +} + +func resourceStorageDefaultObjectAclCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + bucket := d.Get("bucket").(string) + role_entity := make([]interface{}, 0) + + if v, ok := d.GetOk("role_entity"); ok { + role_entity = v.([]interface{}) + } + + if len(role_entity) > 0 { + for _, v := range role_entity { + pair, err := getRoleEntityPair(v.(string)) + + ObjectAccessControl := &storage.ObjectAccessControl{ + Role: pair.Role, + Entity: pair.Entity, + } + + log.Printf("[DEBUG]: setting role = %s, entity = %s on bucket", pair.Role, pair.Entity, bucket) + + _, err = config.clientStorage.DefaultObjectAccessControls.Insert(bucket, ObjectAccessControl).Do() + + if err != nil { + return fmt.Errorf("Error setting Default Object ACL for %s on bucket %s: %v", pair.Entity, bucket, err) + } + } + + return resourceStorageDefaultObjectAclRead(d, meta) + } + return nil +} + +func resourceStorageDefaultObjectAclRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + bucket := d.Get("bucket").(string) + + if _, ok := d.GetOk("role_entity"); ok { + role_entity := make([]interface{}, 0) + re_local := d.Get("role_entity").([]interface{}) + re_local_map := make(map[string]string) + for _, v := range re_local { + res, err := getRoleEntityPair(v.(string)) + + if err != nil { + return fmt.Errorf( + "Old state has malformed Role/Entity pair: %v", err) + } + + re_local_map[res.Entity] = res.Role + } + + res, err := config.clientStorage.DefaultObjectAccessControls.List(bucket).Do() + + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Storage Default Object ACL for bucket %q", d.Get("bucket").(string))) + } + + for _, v := range res.Items { + role := v.Role + entity := v.Entity + // We only store updates to the locally defined access controls + if _, in := re_local_map[entity]; in { + role_entity = append(role_entity, fmt.Sprintf("%s:%s", role, entity)) + log.Printf("[DEBUG]: saving re %s-%s", v.Role, v.Entity) + } + } + + d.Set("role_entity", role_entity) + } + + d.SetId(getDefaultObjectAclId(bucket)) + return nil +} + +func resourceStorageDefaultObjectAclUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + bucket := d.Get("bucket").(string) + + if d.HasChange("role_entity") { + o, n := d.GetChange("role_entity") + old_re := o.([]interface{}) + new_re := n.([]interface{}) + + old_re_map := make(map[string]string) + for _, v := range old_re { + res, err := getRoleEntityPair(v.(string)) + + if err != nil { + return fmt.Errorf( + "Old state has malformed Role/Entity pair: %v", err) + } + + old_re_map[res.Entity] = res.Role + } + + for _, v := range new_re { + pair, err := getRoleEntityPair(v.(string)) + + ObjectAccessControl := &storage.ObjectAccessControl{ + Role: pair.Role, + Entity: pair.Entity, + } + + // If the old state is missing for this entity, it needs to + // be created. Otherwise it is updated + if _, ok := old_re_map[pair.Entity]; ok { + _, err = config.clientStorage.DefaultObjectAccessControls.Update( + bucket, pair.Entity, ObjectAccessControl).Do() + } else { + _, err = config.clientStorage.DefaultObjectAccessControls.Insert( + bucket, ObjectAccessControl).Do() + } + + // Now we only store the keys that have to be removed + delete(old_re_map, pair.Entity) + + if err != nil { + return fmt.Errorf("Error updating Storage Default Object ACL for bucket %s: %v", bucket, err) + } + } + + for entity, _ := range old_re_map { + log.Printf("[DEBUG]: removing entity %s", entity) + err := config.clientStorage.DefaultObjectAccessControls.Delete(bucket, entity).Do() + + if err != nil { + return fmt.Errorf("Error updating Storage Default Object ACL for bucket %s: %v", bucket, err) + } + } + + return resourceStorageDefaultObjectAclRead(d, meta) + } + + return nil +} + +func resourceStorageDefaultObjectAclDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + bucket := d.Get("bucket").(string) + + re_local := d.Get("role_entity").([]interface{}) + for _, v := range re_local { + res, err := getRoleEntityPair(v.(string)) + if err != nil { + return err + } + + log.Printf("[DEBUG]: removing entity %s", res.Entity) + + err = config.clientStorage.DefaultObjectAccessControls.Delete(bucket, res.Entity).Do() + + if err != nil { + return fmt.Errorf("Error deleting entity %s ACL: %s", res.Entity, err) + } + } + + return nil +} diff --git a/google/resource_storage_default_object_acl_test.go b/google/resource_storage_default_object_acl_test.go new file mode 100644 index 00000000000..5af8e5cc479 --- /dev/null +++ b/google/resource_storage_default_object_acl_test.go @@ -0,0 +1,213 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccGoogleStorageDefaultObjectAcl_basic(t *testing.T) { + t.Parallel() + + bucketName := testBucketName() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGoogleStorageDefaultObjectAclDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testGoogleStorageDefaultObjectsAclBasic1(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic1), + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic2), + ), + }, + }, + }) +} + +func TestAccGoogleStorageDefaultObjectAcl_upgrade(t *testing.T) { + t.Parallel() + + bucketName := testBucketName() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGoogleStorageDefaultObjectAclDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testGoogleStorageDefaultObjectsAclBasic1(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic1), + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic2), + ), + }, + + resource.TestStep{ + Config: testGoogleStorageDefaultObjectsAclBasic2(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic2), + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic3_owner), + ), + }, + + resource.TestStep{ + Config: testGoogleStorageDefaultObjectsAclBasicDelete(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleStorageDefaultObjectAclDelete(bucketName, roleEntityBasic1), + testAccCheckGoogleStorageDefaultObjectAclDelete(bucketName, roleEntityBasic2), + testAccCheckGoogleStorageDefaultObjectAclDelete(bucketName, roleEntityBasic3_reader), + ), + }, + }, + }) +} + +func TestAccGoogleStorageDefaultObjectAcl_downgrade(t *testing.T) { + t.Parallel() + + bucketName := testBucketName() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGoogleStorageDefaultObjectAclDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testGoogleStorageDefaultObjectsAclBasic2(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic2), + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic3_owner), + ), + }, + + resource.TestStep{ + Config: testGoogleStorageDefaultObjectsAclBasic3(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic2), + testAccCheckGoogleStorageDefaultObjectAcl(bucketName, roleEntityBasic3_reader), + ), + }, + + resource.TestStep{ + Config: testGoogleStorageDefaultObjectsAclBasicDelete(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleStorageDefaultObjectAclDelete(bucketName, roleEntityBasic1), + testAccCheckGoogleStorageDefaultObjectAclDelete(bucketName, roleEntityBasic2), + testAccCheckGoogleStorageDefaultObjectAclDelete(bucketName, roleEntityBasic3_reader), + ), + }, + }, + }) +} + +func testAccCheckGoogleStorageDefaultObjectAcl(bucket, roleEntityS string) resource.TestCheckFunc { + return func(s *terraform.State) error { + roleEntity, _ := getRoleEntityPair(roleEntityS) + config := testAccProvider.Meta().(*Config) + + res, err := config.clientStorage.DefaultObjectAccessControls.Get(bucket, + roleEntity.Entity).Do() + + if err != nil { + return fmt.Errorf("Error retrieving contents of storage default Acl for bucket %s: %s", bucket, err) + } + + if res.Role != roleEntity.Role { + return fmt.Errorf("Error, Role mismatch %s != %s", res.Role, roleEntity.Role) + } + + return nil + } +} + +func testAccGoogleStorageDefaultObjectAclDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + + if rs.Type != "google_storage_default_object_acl" { + continue + } + + bucket := rs.Primary.Attributes["bucket"] + + _, err := config.clientStorage.DefaultObjectAccessControls.List(bucket).Do() + if err == nil { + return fmt.Errorf("Default Storage Object Acl for bucket %s still exists", bucket) + } + } + return nil +} + +func testAccCheckGoogleStorageDefaultObjectAclDelete(bucket, roleEntityS string) resource.TestCheckFunc { + return func(s *terraform.State) error { + roleEntity, _ := getRoleEntityPair(roleEntityS) + config := testAccProvider.Meta().(*Config) + + _, err := config.clientStorage.DefaultObjectAccessControls.Get(bucket, roleEntity.Entity).Do() + + if err != nil { + return nil + } + + return fmt.Errorf("Error, Object Default Acl Entity still exists %s for bucket %s", + roleEntity.Entity, bucket) + } +} + +func testGoogleStorageDefaultObjectsAclBasicDelete(bucketName string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_storage_default_object_acl" "acl" { + bucket = "${google_storage_bucket.bucket.name}" + role_entity = [] +} +`, bucketName) +} + +func testGoogleStorageDefaultObjectsAclBasic1(bucketName string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_storage_default_object_acl" "acl" { + bucket = "${google_storage_bucket.bucket.name}" + role_entity = ["%s", "%s"] +} +`, bucketName, roleEntityBasic1, roleEntityBasic2) +} + +func testGoogleStorageDefaultObjectsAclBasic2(bucketName string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_storage_default_object_acl" "acl" { + bucket = "${google_storage_bucket.bucket.name}" + role_entity = ["%s", "%s"] +} +`, bucketName, roleEntityBasic2, roleEntityBasic3_owner) +} + +func testGoogleStorageDefaultObjectsAclBasic3(bucketName string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_storage_default_object_acl" "acl" { + bucket = "${google_storage_bucket.bucket.name}" + role_entity = ["%s", "%s"] +} +`, bucketName, roleEntityBasic2, roleEntityBasic3_reader) +} diff --git a/website/docs/r/storage_default_object_acl.html.markdown b/website/docs/r/storage_default_object_acl.html.markdown new file mode 100644 index 00000000000..6dbd25c5843 --- /dev/null +++ b/website/docs/r/storage_default_object_acl.html.markdown @@ -0,0 +1,43 @@ +--- +layout: "google" +page_title: "Google: google_storage_default_object_acl" +sidebar_current: "docs-google-storage-default_object-acl" +description: |- + Creates a new default object ACL in Google Cloud Storage. +--- + +# google\_storage\_default\_object\_acl + +Creates a new default object ACL in Google cloud storage service (GCS). For more information see +[the official documentation](https://cloud.google.com/storage/docs/access-control/lists) +and +[API](https://cloud.google.com/storage/docs/json_api/v1/defaultObjectAccessControls). + +## Example Usage + +Example creating a default object ACL on a bucket with one owner, and one reader. + +```hcl +resource "google_storage_bucket" "image-store" { + name = "image-store-bucket" + location = "EU" +} + +resource "google_storage_default_object_acl" "image-store-default-acl" { + bucket = "${google_storage_bucket.image-store.name}" + role_entity = [ + "OWNER:user-my.email@gmail.com", + "READER:group-mygroup", + ] +} +``` + +## Argument Reference + +* `bucket` - (Required) The name of the bucket it applies to. + +* `role_entity` - (Required) List of role/entity pairs in the form `ROLE:entity`. See [GCS Object ACL documentation](https://cloud.google.com/storage/docs/json_api/v1/objectAccessControls) for more details. + +## Attributes Reference + +Only the arguments listed above are exposed as attributes.