diff --git a/google/resource_storage_bucket.go b/google/resource_storage_bucket.go index f58a5785c29..c355a608dff 100644 --- a/google/resource_storage_bucket.go +++ b/google/resource_storage_bucket.go @@ -1,12 +1,14 @@ package google import ( + "bytes" "errors" "fmt" "log" "strings" "time" + "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" @@ -77,6 +79,68 @@ func resourceStorageBucket() *schema.Resource { ForceNew: true, }, + "lifecycle_rule": { + Type: schema.TypeList, + Optional: true, + MaxItems: 100, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + MaxItems: 1, + Set: resourceGCSBucketLifecycleRuleActionHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + }, + "storage_class": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "condition": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + MaxItems: 1, + Set: resourceGCSBucketLifecycleRuleConditionHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "age": { + Type: schema.TypeInt, + Optional: true, + }, + "created_before": { + Type: schema.TypeString, + Optional: true, + }, + "is_live": { + Type: schema.TypeBool, + Optional: true, + }, + "matches_storage_class": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "num_newer_versions": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "website": &schema.Schema{ Type: schema.TypeList, Optional: true, @@ -150,6 +214,10 @@ func resourceStorageBucketCreate(d *schema.ResourceData, meta interface{}) error sb.StorageClass = v.(string) } + if err := resourceGCSBucketLifecycleCreateOrUpdate(d, sb); err != nil { + return err + } + if v, ok := d.GetOk("website"); ok { websites := v.([]interface{}) @@ -208,6 +276,12 @@ func resourceStorageBucketUpdate(d *schema.ResourceData, meta interface{}) error sb := &storage.Bucket{} + if d.HasChange("lifecycle_rule") { + if err := resourceGCSBucketLifecycleCreateOrUpdate(d, sb); err != nil { + return err + } + } + if d.HasChange("website") { if v, ok := d.GetOk("website"); ok { websites := v.([]interface{}) @@ -382,3 +456,129 @@ func flattenCors(corsRules []*storage.BucketCors) []map[string]interface{} { } return corsRulesSchema } + +func resourceGCSBucketLifecycleCreateOrUpdate(d *schema.ResourceData, sb *storage.Bucket) error { + if v, ok := d.GetOk("lifecycle_rule"); ok { + lifecycle_rules := v.([]interface{}) + + sb.Lifecycle = &storage.BucketLifecycle{} + sb.Lifecycle.Rule = make([]*storage.BucketLifecycleRule, 0, len(lifecycle_rules)) + + for _, raw_lifecycle_rule := range lifecycle_rules { + lifecycle_rule := raw_lifecycle_rule.(map[string]interface{}) + + target_lifecycle_rule := &storage.BucketLifecycleRule{} + + if v, ok := lifecycle_rule["action"]; ok { + if actions := v.(*schema.Set).List(); len(actions) == 1 { + action := actions[0].(map[string]interface{}) + + target_lifecycle_rule.Action = &storage.BucketLifecycleRuleAction{} + + if v, ok := action["type"]; ok { + target_lifecycle_rule.Action.Type = v.(string) + } + + if v, ok := action["storage_class"]; ok { + target_lifecycle_rule.Action.StorageClass = v.(string) + } + } else { + return fmt.Errorf("Exactly one action is required") + } + } + + if v, ok := lifecycle_rule["condition"]; ok { + if conditions := v.(*schema.Set).List(); len(conditions) == 1 { + condition := conditions[0].(map[string]interface{}) + + target_lifecycle_rule.Condition = &storage.BucketLifecycleRuleCondition{} + + if v, ok := condition["age"]; ok { + target_lifecycle_rule.Condition.Age = int64(v.(int)) + } + + if v, ok := condition["created_before"]; ok { + target_lifecycle_rule.Condition.CreatedBefore = v.(string) + } + + if v, ok := condition["is_live"]; ok { + target_lifecycle_rule.Condition.IsLive = v.(bool) + } + + if v, ok := condition["matches_storage_class"]; ok { + matches_storage_classes := v.([]interface{}) + + target_matches_storage_classes := make([]string, 0, len(matches_storage_classes)) + + for _, v := range matches_storage_classes { + target_matches_storage_classes = append(target_matches_storage_classes, v.(string)) + } + + target_lifecycle_rule.Condition.MatchesStorageClass = target_matches_storage_classes + } + + if v, ok := condition["num_newer_versions"]; ok { + target_lifecycle_rule.Condition.NumNewerVersions = int64(v.(int)) + } + } else { + return fmt.Errorf("Exactly one condition is required") + } + } + + sb.Lifecycle.Rule = append(sb.Lifecycle.Rule, target_lifecycle_rule) + } + } + + return nil +} + +func resourceGCSBucketLifecycleRuleActionHash(v interface{}) int { + if v == nil { + return 0 + } + + var buf bytes.Buffer + m := v.(map[string]interface{}) + + buf.WriteString(fmt.Sprintf("%s-", m["type"].(string))) + + if v, ok := m["storage_class"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + return hashcode.String(buf.String()) +} + +func resourceGCSBucketLifecycleRuleConditionHash(v interface{}) int { + if v == nil { + return 0 + } + + var buf bytes.Buffer + m := v.(map[string]interface{}) + + if v, ok := m["age"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(int))) + } + + if v, ok := m["created_before"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := m["is_live"]; ok { + buf.WriteString(fmt.Sprintf("%t-", v.(bool))) + } + + if v, ok := m["matches_storage_class"]; ok { + matches_storage_classes := v.([]interface{}) + for _, matches_storage_class := range matches_storage_classes { + buf.WriteString(fmt.Sprintf("%s-", matches_storage_class)) + } + } + + if v, ok := m["num_newer_versions"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(int))) + } + + return hashcode.String(buf.String()) +} diff --git a/google/resource_storage_bucket_test.go b/google/resource_storage_bucket_test.go index d1055a00001..a0df751b65d 100644 --- a/google/resource_storage_bucket_test.go +++ b/google/resource_storage_bucket_test.go @@ -82,6 +82,52 @@ func TestAccStorageBucket_customAttributes(t *testing.T) { }) } +func TestAccStorageBucket_lifecycleRules(t *testing.T) { + var bucket storage.Bucket + bucketName := fmt.Sprintf("tf-test-acc-bucket-%d", acctest.RandInt()) + + hash_step0_lc0_action := resourceGCSBucketLifecycleRuleActionHash(map[string]interface{}{"type": "SetStorageClass", "storage_class": "NEARLINE"}) + hash_step0_lc0_condition := resourceGCSBucketLifecycleRuleConditionHash(map[string]interface{}{"age": 2, "created_before": "", "is_live": false, "num_newer_versions": 0}) + + hash_step0_lc1_action := resourceGCSBucketLifecycleRuleActionHash(map[string]interface{}{"type": "Delete", "storage_class": ""}) + hash_step0_lc1_condition := resourceGCSBucketLifecycleRuleConditionHash(map[string]interface{}{"age": 10, "created_before": "", "is_live": false, "num_newer_versions": 0}) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccStorageBucketDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccStorageBucket_lifecycleRules(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStorageBucketExists( + "google_storage_bucket.bucket", bucketName, &bucket), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.#", "2"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.0.action.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.0.action.%d.type", hash_step0_lc0_action), "SetStorageClass"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.0.action.%d.storage_class", hash_step0_lc0_action), "NEARLINE"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.0.condition.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.0.condition.%d.age", hash_step0_lc0_condition), "2"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.1.action.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.1.action.%d.type", hash_step0_lc1_action), "Delete"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.1.condition.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.1.condition.%d.age", hash_step0_lc1_condition), "10"), + ), + }, + }, + }) +} + func TestAccStorageBucket_storageClass(t *testing.T) { var bucket storage.Bucket bucketName := fmt.Sprintf("tf-test-acc-bucket-%d", acctest.RandInt()) @@ -128,6 +174,15 @@ func TestAccStorageBucket_update(t *testing.T) { var bucket storage.Bucket bucketName := fmt.Sprintf("tf-test-acl-bucket-%d", acctest.RandInt()) + hash_step2_lc0_action := resourceGCSBucketLifecycleRuleActionHash(map[string]interface{}{"type": "Delete", "storage_class": ""}) + hash_step2_lc0_condition := resourceGCSBucketLifecycleRuleConditionHash(map[string]interface{}{"age": 10, "created_before": "", "is_live": false, "num_newer_versions": 0}) + + hash_step3_lc0_action := resourceGCSBucketLifecycleRuleActionHash(map[string]interface{}{"type": "SetStorageClass", "storage_class": "NEARLINE"}) + hash_step3_lc0_condition := resourceGCSBucketLifecycleRuleConditionHash(map[string]interface{}{"age": 2, "created_before": "", "is_live": false, "num_newer_versions": 0}) + + hash_step3_lc1_action := resourceGCSBucketLifecycleRuleActionHash(map[string]interface{}{"type": "Delete", "storage_class": ""}) + hash_step3_lc1_condition := resourceGCSBucketLifecycleRuleConditionHash(map[string]interface{}{"age": 10, "created_before": "", "is_live": false, "num_newer_versions": 2}) + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -142,6 +197,81 @@ func TestAccStorageBucket_update(t *testing.T) { "google_storage_bucket.bucket", "location", "US"), resource.TestCheckResourceAttr( "google_storage_bucket.bucket", "force_destroy", "false"), + resource.TestCheckNoResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.#"), + ), + }, + resource.TestStep{ + Config: testAccStorageBucket_customAttributes(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStorageBucketExists( + "google_storage_bucket.bucket", bucketName, &bucket), + 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"), + resource.TestCheckNoResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.#"), + ), + }, + resource.TestStep{ + Config: testAccStorageBucket_customAttributes_withLifecycle1(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStorageBucketExists( + "google_storage_bucket.bucket", bucketName, &bucket), + 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"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.0.action.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.0.action.%d.type", hash_step2_lc0_action), "Delete"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.0.condition.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.0.condition.%d.age", hash_step2_lc0_condition), "10"), + ), + }, + resource.TestStep{ + Config: testAccStorageBucket_customAttributes_withLifecycle2(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStorageBucketExists( + "google_storage_bucket.bucket", bucketName, &bucket), + 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"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.#", "2"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.0.action.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.0.action.%d.type", hash_step3_lc0_action), "SetStorageClass"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.0.action.%d.storage_class", hash_step3_lc0_action), "NEARLINE"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.0.condition.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.0.condition.%d.age", hash_step3_lc0_condition), "2"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.1.action.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.1.action.%d.type", hash_step3_lc1_action), "Delete"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.1.condition.#", "1"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.1.condition.%d.age", hash_step3_lc1_condition), "10"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", fmt.Sprintf("lifecycle_rule.1.condition.%d.num_newer_versions", hash_step3_lc1_condition), "2"), ), }, resource.TestStep{ @@ -155,6 +285,8 @@ func TestAccStorageBucket_update(t *testing.T) { "google_storage_bucket.bucket", "location", "EU"), resource.TestCheckResourceAttr( "google_storage_bucket.bucket", "force_destroy", "true"), + resource.TestCheckResourceAttr( + "google_storage_bucket.bucket", "lifecycle_rule.#", "0"), ), }, }, @@ -375,6 +507,54 @@ resource "google_storage_bucket" "bucket" { `, bucketName) } +func testAccStorageBucket_customAttributes_withLifecycle1(bucketName string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" + predefined_acl = "publicReadWrite" + location = "EU" + force_destroy = "true" + lifecycle_rule { + action { + type = "Delete" + } + condition { + age = 10 + } + } +} +`, bucketName) +} + +func testAccStorageBucket_customAttributes_withLifecycle2(bucketName string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" + predefined_acl = "publicReadWrite" + location = "EU" + force_destroy = "true" + lifecycle_rule { + action { + type = "SetStorageClass" + storage_class = "NEARLINE" + } + condition { + age = 2 + } + } + lifecycle_rule { + action { + type = "Delete" + } + condition { + age = 10 + num_newer_versions = 2 + } + } +} +`, bucketName) +} + func testAccStorageBucket_storageClass(bucketName, storageClass, location string) string { var locationBlock string if location != "" { @@ -409,3 +589,28 @@ resource "google_storage_bucket" "bucket" { } `, bucketName) } + +func testAccStorageBucket_lifecycleRules(bucketName string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" + lifecycle_rule { + action { + type = "SetStorageClass" + storage_class = "NEARLINE" + } + condition { + age = 2 + } + } + lifecycle_rule { + action { + type = "Delete" + } + condition { + age = 10 + } + } +} +`, bucketName) +} diff --git a/website/docs/r/storage_bucket.html.markdown b/website/docs/r/storage_bucket.html.markdown index fb87f240489..c4692a385c8 100644 --- a/website/docs/r/storage_bucket.html.markdown +++ b/website/docs/r/storage_bucket.html.markdown @@ -56,10 +56,36 @@ to `google_storage_bucket_acl.predefined_acl`. * `storage_class` - (Optional) The [Storage Class](https://cloud.google.com/storage/docs/storage-classes) of the new bucket. Supported values include: `MULTI_REGIONAL`, `REGIONAL`, `NEARLINE`, `COLDLINE`. +* `lifecycle_rule` - (Optional) The bucket's [Lifecycle Rules](https://cloud.google.com/storage/docs/lifecycle#configuration) configuration. Multiple blocks of this type are permitted. Structure is documented below. + * `website` - (Optional) Configuration if the bucket acts as a website. Structure is documented below. * `cors` - (Optional) The bucket's [Cross-Origin Resource Sharing (CORS)](https://www.w3.org/TR/cors/) configuration. Multiple blocks of this type are permitted. Structure is documented below. +The `lifecycle_rule` block supports: + +* `action` - (Required) The Lifecycle Rule's action configuration. A single block of this type is supported. Structure is documented below. + +* `condition` - (Required) The Lifecycle Rule's condition configuration. A single block of this type is supported. Structure is documented below. + +The `action` block supports: + +* `type` - The type of the action of this Lifecycle Rule. Supported values include: `Delete` and `SetStorageClass`. + +* `storage_class` - (Required if action type is `SetStorageClass`) The target [Storage Class](https://cloud.google.com/storage/docs/storage-classes) of objects affected by this Lifecycle Rule. Supported values include: `MULTI_REGIONAL`, `REGIONAL`, `NEARLINE`, `COLDLINE`. + +The `condition` block supports the following elements, and requires at least one to be defined: + +* `age` - (Optional) Minimum age of an object in days to satisfy this condition. + +* `created_before` - (Optional) Creation date of an object in RFC 3339 (e.g. `2017-06-13`) to satisfy this condition. + +* `is_live` - (Optional) Relevant only for versioned objects. If `true`, this condition matches live objects, archived objects otherwise. + +* `matches_storage_class` - (Optional) [Storage Class](https://cloud.google.com/storage/docs/storage-classes) of objects to satisfy this condition. Supported values include: `MULTI_REGIONAL`, `REGIONAL`, `NEARLINE`, `COLDLINE`, `STANDARD`, `DURABLE_REDUCED_AVAILABILITY`. + +* `num_newer_versions` - (Optional) Relevant only for versioned objects. The number of newer versions of an object to satisfy this condition. + The `website` block supports: * `main_page_suffix` - (Optional) Behaves as the bucket's directory index where