diff --git a/google/provider.go b/google/provider.go index 5850e5810de..4e1e0c0fe69 100644 --- a/google/provider.go +++ b/google/provider.go @@ -193,6 +193,7 @@ func Provider() terraform.ResourceProvider { "google_storage_bucket_object": resourceStorageBucketObject(), "google_storage_object_acl": resourceStorageObjectAcl(), "google_storage_default_object_acl": resourceStorageDefaultObjectAcl(), + "google_storage_notification": resourceStorageNotification(), }, ConfigureFunc: providerConfigure, diff --git a/google/resource_storage_notification.go b/google/resource_storage_notification.go new file mode 100644 index 00000000000..c42651f1300 --- /dev/null +++ b/google/resource_storage_notification.go @@ -0,0 +1,146 @@ +package google + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "google.golang.org/api/storage/v1" +) + +func resourceStorageNotification() *schema.Resource { + return &schema.Resource{ + Create: resourceStorageNotificationCreate, + Read: resourceStorageNotificationRead, + Delete: resourceStorageNotificationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "bucket": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "payload_format": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"JSON_API_V1", "NONE"}, false), + }, + + "topic": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + }, + + "custom_attributes": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "event_types": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + "OBJECT_FINALIZE", "OBJECT_METADATA_UPDATE", "OBJECT_DELETE", "OBJECT_ARCHIVE"}, + false), + }, + }, + + "object_name_prefix": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceStorageNotificationCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + bucket := d.Get("bucket").(string) + + project, err := getProject(d, config) + if err != nil { + return err + } + + computedTopicName := getComputedTopicName(project, d.Get("topic").(string)) + + storageNotification := &storage.Notification{ + CustomAttributes: expandStringMap(d, "custom_attributes"), + EventTypes: convertStringSet(d.Get("event_types").(*schema.Set)), + ObjectNamePrefix: d.Get("object_name_prefix").(string), + PayloadFormat: d.Get("payload_format").(string), + Topic: computedTopicName, + } + + res, err := config.clientStorage.Notifications.Insert(bucket, storageNotification).Do() + if err != nil { + return fmt.Errorf("Error creating notification config for bucket %s: %v", bucket, err) + } + + d.SetId(fmt.Sprintf("%s/notificationConfigs/%s", bucket, res.Id)) + + return resourceStorageNotificationRead(d, meta) +} + +func resourceStorageNotificationRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + bucket, notificationID := resourceStorageNotificationParseID(d.Id()) + + res, err := config.clientStorage.Notifications.Get(bucket, notificationID).Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Notification configuration %s for bucket %s", notificationID, bucket)) + } + + d.Set("bucket", bucket) + d.Set("payload_format", res.PayloadFormat) + d.Set("topic", res.Topic) + d.Set("object_name_prefix", res.ObjectNamePrefix) + d.Set("event_types", res.EventTypes) + d.Set("self_link", res.SelfLink) + d.Set("custom_attributes", res.CustomAttributes) + + return nil +} + +func resourceStorageNotificationDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + bucket, notificationID := resourceStorageNotificationParseID(d.Id()) + + err := config.clientStorage.Notifications.Delete(bucket, notificationID).Do() + if err != nil { + return fmt.Errorf("Error deleting notification configuration %s for bucket %s: %v", notificationID, bucket, err) + } + + return nil +} + +func resourceStorageNotificationParseID(id string) (string, string) { + //bucket, NotificationID + parts := strings.Split(id, "/") + + return parts[0], parts[2] +} diff --git a/google/resource_storage_notification_test.go b/google/resource_storage_notification_test.go new file mode 100644 index 00000000000..18a93cda3c0 --- /dev/null +++ b/google/resource_storage_notification_test.go @@ -0,0 +1,247 @@ +package google + +import ( + "fmt" + "os" + "reflect" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/storage/v1" +) + +var ( + gcsServiceAccount = fmt.Sprintf("serviceAccount:%s@gs-project-accounts.iam.gserviceaccount.com", os.Getenv("GOOGLE_PROJECT")) + payload = "JSON_API_V1" +) + +func TestAccGoogleStorageNotification_basic(t *testing.T) { + t.Parallel() + + skipIfEnvNotSet(t, "GOOGLE_PROJECT") + + var notification storage.Notification + bucketName := testBucketName() + topicName := fmt.Sprintf("tf-pstopic-test-%d", acctest.RandInt()) + topic := fmt.Sprintf("//pubsub.googleapis.com/projects/%s/topics/%s", os.Getenv("GOOGLE_PROJECT"), topicName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGoogleStorageNotificationDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testGoogleStorageNotificationBasic(bucketName, topicName, topic), + Check: resource.ComposeTestCheckFunc( + testAccCheckStorageNotificationExists( + "google_storage_notification.notification", ¬ification), + resource.TestCheckResourceAttr( + "google_storage_notification.notification", "bucket", bucketName), + resource.TestCheckResourceAttr( + "google_storage_notification.notification", "topic", topic), + resource.TestCheckResourceAttr( + "google_storage_notification.notification", "payload_format", payload), + resource.TestCheckResourceAttr( + "google_storage_notification.notification_with_prefix", "object_name_prefix", "foobar"), + ), + }, + resource.TestStep{ + ResourceName: "google_storage_notification.notification", + ImportState: true, + ImportStateVerify: true, + }, + resource.TestStep{ + ResourceName: "google_storage_notification.notification_with_prefix", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccGoogleStorageNotification_withEventsAndAttributes(t *testing.T) { + t.Parallel() + + skipIfEnvNotSet(t, "GOOGLE_PROJECT") + + var notification storage.Notification + bucketName := testBucketName() + topicName := fmt.Sprintf("tf-pstopic-test-%d", acctest.RandInt()) + topic := fmt.Sprintf("//pubsub.googleapis.com/projects/%s/topics/%s", os.Getenv("GOOGLE_PROJECT"), topicName) + eventType1 := "OBJECT_FINALIZE" + eventType2 := "OBJECT_ARCHIVE" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGoogleStorageNotificationDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testGoogleStorageNotificationOptionalEventsAttributes(bucketName, topicName, topic, eventType1, eventType2), + Check: resource.ComposeTestCheckFunc( + testAccCheckStorageNotificationExists( + "google_storage_notification.notification", ¬ification), + resource.TestCheckResourceAttr( + "google_storage_notification.notification", "bucket", bucketName), + resource.TestCheckResourceAttr( + "google_storage_notification.notification", "topic", topic), + resource.TestCheckResourceAttr( + "google_storage_notification.notification", "payload_format", payload), + testAccCheckStorageNotificationCheckEventType( + ¬ification, []string{eventType1, eventType2}), + testAccCheckStorageNotificationCheckAttributes( + ¬ification, "new-attribute", "new-attribute-value"), + ), + }, + resource.TestStep{ + ResourceName: "google_storage_notification.notification", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccGoogleStorageNotificationDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_storage_notification" { + continue + } + + bucket, notificationID := resourceStorageNotificationParseID(rs.Primary.ID) + + _, err := config.clientStorage.Notifications.Get(bucket, notificationID).Do() + if err == nil { + return fmt.Errorf("Notification configuration still exists") + } + } + + return nil +} + +func testAccCheckStorageNotificationExists(resource string, notification *storage.Notification) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("Not found: %s", resource) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + bucket, notificationID := resourceStorageNotificationParseID(rs.Primary.ID) + + found, err := config.clientStorage.Notifications.Get(bucket, notificationID).Do() + if err != nil { + return err + } + + if found.Id != notificationID { + return fmt.Errorf("Storage notification configuration not found") + } + + *notification = *found + + return nil + } +} + +func testAccCheckStorageNotificationCheckEventType(notification *storage.Notification, eventTypes []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if !reflect.DeepEqual(notification.EventTypes, eventTypes) { + return fmt.Errorf("Target event types are incorrect. Expected %s, got %s", eventTypes, notification.EventTypes) + } + return nil + } +} + +func testAccCheckStorageNotificationCheckAttributes(notification *storage.Notification, key, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + val, ok := notification.CustomAttributes[key] + if !ok { + return fmt.Errorf("Custom attribute with key %s not found", key) + } + + if val != value { + return fmt.Errorf("Custom attribute value did not match for key %s: expected %s but found %s", key, value, val) + } + return nil + } +} + +func testGoogleStorageNotificationBasic(bucketName, topicName, topic string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_pubsub_topic" "topic" { + name = "%s" +} +// We have to provide GCS default storage account with the permission +// to publish to a Cloud Pub/Sub topic from this project +// Otherwise notification configuration won't work +resource "google_pubsub_topic_iam_binding" "binding" { + topic = "${google_pubsub_topic.topic.name}" + role = "roles/pubsub.publisher" + + members = ["%s"] +} + +resource "google_storage_notification" "notification" { + bucket = "${google_storage_bucket.bucket.name}" + payload_format = "JSON_API_V1" + topic = "${google_pubsub_topic.topic.id}" + depends_on = ["google_pubsub_topic_iam_binding.binding"] +} + +resource "google_storage_notification" "notification_with_prefix" { + bucket = "${google_storage_bucket.bucket.name}" + payload_format = "JSON_API_V1" + topic = "${google_pubsub_topic.topic.id}" + object_name_prefix = "foobar" + depends_on = ["google_pubsub_topic_iam_binding.binding"] +} + +`, bucketName, topicName, gcsServiceAccount) +} + +func testGoogleStorageNotificationOptionalEventsAttributes(bucketName, topicName, topic, eventType1, eventType2 string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_pubsub_topic" "topic" { + name = "%s" +} +// We have to provide GCS default storage account with the permission +// to publish to a Cloud Pub/Sub topic from this project +// Otherwise notification configuration won't work +resource "google_pubsub_topic_iam_binding" "binding" { + topic = "${google_pubsub_topic.topic.name}" + role = "roles/pubsub.publisher" + + members = ["%s"] +} + +resource "google_storage_notification" "notification" { + bucket = "${google_storage_bucket.bucket.name}" + payload_format = "JSON_API_V1" + topic = "${google_pubsub_topic.topic.id}" + event_types = ["%s","%s"] + custom_attributes { + new-attribute = "new-attribute-value" + } + depends_on = ["google_pubsub_topic_iam_binding.binding"] +} + +`, bucketName, topicName, gcsServiceAccount, eventType1, eventType2) +} diff --git a/website/docs/r/storage_notification.html.markdown b/website/docs/r/storage_notification.html.markdown new file mode 100644 index 00000000000..8e8595e2bbd --- /dev/null +++ b/website/docs/r/storage_notification.html.markdown @@ -0,0 +1,86 @@ +--- +layout: "google" +page_title: "Google: google_storage_notification" +sidebar_current: "docs-google-storage-notification" +description: |- + Creates a new notification configuration on a specified bucket. +--- + +# google\_storage\_notification + +Creates a new notification configuration on a specified bucket, establishing a flow of event notifications from GCS to a Cloud Pub/Sub topic. + For more information see +[the official documentation](https://cloud.google.com/storage/docs/pubsub-notifications) +and +[API](https://cloud.google.com/storage/docs/json_api/v1/notifications). + +## Example Usage + +```hcl +resource "google_storage_bucket" "bucket" { + name = "default_bucket" +} + +resource "google_pubsub_topic" "topic" { + name = "default_topic" +} + +// In order to enable notifications, +// a GCS service account unique to each project +// must have the IAM permission "projects.topics.publish" to a Cloud Pub/Sub topic from this project +// The only reference to this requirement can be found here: +// https://cloud.google.com/storage/docs/gsutil/commands/notification +// The GCS service account has the format of @gs-project-accounts.iam.gserviceaccount.com +// API for retrieving it https://cloud.google.com/storage/docs/json_api/v1/projects/serviceAccount/get + +resource "google_pubsub_topic_iam_binding" "binding" { + topic = "${google_pubsub_topic.topic.name}" + role = "roles/pubsub.publisher" + + members = ["serviceAccount:my-project-id@gs-project-accounts.iam.gserviceaccount.com"] +} + +resource "google_storage_notification" "notification" { + bucket = "${google_storage_bucket.bucket.name}" + payload_format = "JSON_API_V1" + topic = "${google_pubsub_topic.topic.id}" + event_types = ["%s","%s"] + custom_attributes { + new-attribute = "new-attribute-value" + } + depends_on = ["google_pubsub_topic_iam_binding.binding"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `bucket` - (Required) The name of the bucket. + +* `payload_format` - (Required) The desired content of the Payload. One of `"JSON_API_V1"` or `"NONE"`. + +* `topic` - (Required) The Cloud PubSub topic to which this subscription publishes. + +- - - + +* `custom_attributes` - (Optional) A set of key/value attribute pairs to attach to each Cloud PubSub message published for this notification subscription + +* `event_types` - (Optional) List of event type filters for this notification config. If not specified, Cloud Storage will send notifications for all event types. The valid types are: `"OBJECT_FINALIZE"`, `"OBJECT_METADATA_UPDATE"`, `"OBJECT_DELETE"`, `"OBJECT_ARCHIVE"` + +* `object_name_prefix` - (Optional) Specifies a prefix path filter for this notification config. Cloud Storage will only send notifications for objects in this bucket whose names begin with the specified prefix. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `self_link` - The URI of the created resource. + +## Import + +Storage notifications can be imported using the notification `id` in the format `/notificationConfigs/` e.g. + +``` +$ terraform import google_storage_notification.notification default_bucket/notificationConfigs/102 +``` diff --git a/website/google.erb b/website/google.erb index 99590557069..52cb276645e 100644 --- a/website/google.erb +++ b/website/google.erb @@ -526,6 +526,10 @@ google_storage_default_object_acl + > + google_storage_notification + + > google_storage_object_acl