From 8231d6a83a96def6e8d14ba2f6a65174a544f8e3 Mon Sep 17 00:00:00 2001 From: Joe Selman Date: Tue, 26 Sep 2017 15:26:54 -0700 Subject: [PATCH] Add resource_logging_billing_account_sink resource (#457) * Add resource_logging_billing_account_sink resource * Fix bad style --- google/provider.go | 1 + .../resource_logging_billing_account_sink.go | 75 ++++++++ ...ource_logging_billing_account_sink_test.go | 166 ++++++++++++++++++ google/resource_logging_project_sink.go | 90 ++-------- google/resource_logging_sink.go | 70 ++++++++ ...logging_billing_account_sink.html.markdown | 69 ++++++++ .../docs/r/logging_project_sink.html.markdown | 2 +- website/google.erb | 4 + 8 files changed, 405 insertions(+), 72 deletions(-) create mode 100644 google/resource_logging_billing_account_sink.go create mode 100644 google/resource_logging_billing_account_sink_test.go create mode 100644 google/resource_logging_sink.go create mode 100644 website/docs/r/logging_billing_account_sink.html.markdown diff --git a/google/provider.go b/google/provider.go index 472ece21d41..0a202fe9ca3 100644 --- a/google/provider.go +++ b/google/provider.go @@ -108,6 +108,7 @@ func Provider() terraform.ResourceProvider { "google_dns_record_set": resourceDnsRecordSet(), "google_folder": resourceGoogleFolder(), "google_folder_iam_policy": resourceGoogleFolderIamPolicy(), + "google_logging_billing_account_sink": resourceLoggingBillingAccountSink(), "google_logging_project_sink": resourceLoggingProjectSink(), "google_sourcerepo_repository": resourceSourceRepoRepository(), "google_spanner_instance": resourceSpannerInstance(), diff --git a/google/resource_logging_billing_account_sink.go b/google/resource_logging_billing_account_sink.go new file mode 100644 index 00000000000..53cbcb49af1 --- /dev/null +++ b/google/resource_logging_billing_account_sink.go @@ -0,0 +1,75 @@ +package google + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceLoggingBillingAccountSink() *schema.Resource { + schm := &schema.Resource{ + Create: resourceLoggingBillingAccountSinkCreate, + Read: resourceLoggingBillingAccountSinkRead, + Delete: resourceLoggingBillingAccountSinkDelete, + Update: resourceLoggingBillingAccountSinkUpdate, + Schema: resourceLoggingSinkSchema(), + } + schm.Schema["billing_account"] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + } + return schm +} + +func resourceLoggingBillingAccountSinkCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + id, sink := expandResourceLoggingSink(d, "billingAccounts", d.Get("billing_account").(string)) + + // The API will reject any requests that don't explicitly set 'uniqueWriterIdentity' to true. + _, err := config.clientLogging.BillingAccounts.Sinks.Create(id.parent(), sink).UniqueWriterIdentity(true).Do() + if err != nil { + return err + } + + d.SetId(id.canonicalId()) + return resourceLoggingBillingAccountSinkRead(d, meta) +} + +func resourceLoggingBillingAccountSinkRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + sink, err := config.clientLogging.BillingAccounts.Sinks.Get(d.Id()).Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Billing Logging Sink %s", d.Get("name").(string))) + } + + flattenResourceLoggingSink(d, sink) + return nil + +} + +func resourceLoggingBillingAccountSinkUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + sink := expandResourceLoggingSinkForUpdate(d) + + // The API will reject any requests that don't explicitly set 'uniqueWriterIdentity' to true. + _, err := config.clientLogging.BillingAccounts.Sinks.Patch(d.Id(), sink).UniqueWriterIdentity(true).Do() + if err != nil { + return err + } + + return resourceLoggingBillingAccountSinkRead(d, meta) +} + +func resourceLoggingBillingAccountSinkDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + _, err := config.clientLogging.Projects.Sinks.Delete(d.Id()).Do() + if err != nil { + return err + } + + return nil +} diff --git a/google/resource_logging_billing_account_sink_test.go b/google/resource_logging_billing_account_sink_test.go new file mode 100644 index 00000000000..9933a4085f9 --- /dev/null +++ b/google/resource_logging_billing_account_sink_test.go @@ -0,0 +1,166 @@ +package google + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/logging/v2" +) + +func TestAccLoggingBillingAccountSink_basic(t *testing.T) { + skipIfEnvNotSet(t, "GOOGLE_BILLING_ACCOUNT") + + sinkName := "tf-test-sink-" + acctest.RandString(10) + bucketName := "tf-test-sink-bucket-" + acctest.RandString(10) + billingAccount := os.Getenv("GOOGLE_BILLING_ACCOUNT") + + var sink logging.LogSink + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLoggingBillingAccountSinkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccLoggingBillingAccountSink_basic(sinkName, bucketName, billingAccount), + Check: resource.ComposeTestCheckFunc( + testAccCheckLoggingBillingAccountSinkExists("google_logging_billing_account_sink.basic", &sink), + testAccCheckLoggingBillingAccountSink(&sink, "google_logging_billing_account_sink.basic"), + ), + }, + }, + }) +} + +func TestAccLoggingBillingAccountSink_update(t *testing.T) { + skipIfEnvNotSet(t, "GOOGLE_BILLING_ACCOUNT") + + sinkName := "tf-test-sink-" + acctest.RandString(10) + bucketName := "tf-test-sink-bucket-" + acctest.RandString(10) + updatedBucketName := "tf-test-sink-bucket-" + acctest.RandString(10) + billingAccount := os.Getenv("GOOGLE_BILLING_ACCOUNT") + + var sinkBefore, sinkAfter logging.LogSink + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLoggingBillingAccountSinkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccLoggingBillingAccountSink_update(sinkName, bucketName, billingAccount), + Check: resource.ComposeTestCheckFunc( + testAccCheckLoggingBillingAccountSinkExists("google_logging_billing_account_sink.update", &sinkBefore), + testAccCheckLoggingBillingAccountSink(&sinkBefore, "google_logging_billing_account_sink.update"), + ), + }, { + Config: testAccLoggingBillingAccountSink_update(sinkName, updatedBucketName, billingAccount), + Check: resource.ComposeTestCheckFunc( + testAccCheckLoggingBillingAccountSinkExists("google_logging_billing_account_sink.update", &sinkAfter), + testAccCheckLoggingBillingAccountSink(&sinkAfter, "google_logging_billing_account_sink.update"), + ), + }, + }, + }) + + // Destination should have changed, but WriterIdentity should be the same + if sinkBefore.Destination == sinkAfter.Destination { + t.Errorf("Expected Destination to change, but it didn't: Destination = %#v", sinkBefore.Destination) + } + if sinkBefore.WriterIdentity != sinkAfter.WriterIdentity { + t.Errorf("Expected WriterIdentity to be the same, but it differs: before = %#v, after = %#v", + sinkBefore.WriterIdentity, sinkAfter.WriterIdentity) + } +} + +func testAccCheckLoggingBillingAccountSinkDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_logging_billing_account_sink" { + continue + } + + attributes := rs.Primary.Attributes + + _, err := config.clientLogging.BillingAccounts.Sinks.Get(attributes["id"]).Do() + if err == nil { + return fmt.Errorf("billing sink still exists") + } + } + + return nil +} + +func testAccCheckLoggingBillingAccountSinkExists(n string, sink *logging.LogSink) resource.TestCheckFunc { + return func(s *terraform.State) error { + attributes, err := getResourceAttributes(n, s) + if err != nil { + return err + } + config := testAccProvider.Meta().(*Config) + + si, err := config.clientLogging.BillingAccounts.Sinks.Get(attributes["id"]).Do() + if err != nil { + return err + } + *sink = *si + + return nil + } +} + +func testAccCheckLoggingBillingAccountSink(sink *logging.LogSink, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + attributes, err := getResourceAttributes(n, s) + if err != nil { + return err + } + + if sink.Destination != attributes["destination"] { + return fmt.Errorf("mismatch on destination: api has %s but client has %s", sink.Destination, attributes["destination"]) + } + + if sink.Filter != attributes["filter"] { + return fmt.Errorf("mismatch on filter: api has %s but client has %s", sink.Filter, attributes["filter"]) + } + + if sink.WriterIdentity != attributes["writer_identity"] { + return fmt.Errorf("mismatch on writer_identity: api has %s but client has %s", sink.WriterIdentity, attributes["writer_identity"]) + } + + return nil + } +} + +func testAccLoggingBillingAccountSink_basic(name, bucketName, billingAccount string) string { + return fmt.Sprintf(` +resource "google_logging_billing_account_sink" "basic" { + name = "%s" + billing_account = "%s" + destination = "storage.googleapis.com/${google_storage_bucket.log-bucket.name}" + filter = "logName=\"projects/%s/logs/compute.googleapis.com%%2Factivity_log\" AND severity>=ERROR" +} + +resource "google_storage_bucket" "log-bucket" { + name = "%s" +}`, name, billingAccount, getTestProjectFromEnv(), bucketName) +} + +func testAccLoggingBillingAccountSink_update(name, bucketName, billingAccount string) string { + return fmt.Sprintf(` +resource "google_logging_billing_account_sink" "update" { + name = "%s" + billing_account = "%s" + destination = "storage.googleapis.com/${google_storage_bucket.log-bucket.name}" + filter = "logName=\"projects/%s/logs/compute.googleapis.com%%2Factivity_log\" AND severity>=ERROR" +} + +resource "google_storage_bucket" "log-bucket" { + name = "%s" +}`, name, billingAccount, getTestProjectFromEnv(), bucketName) +} diff --git a/google/resource_logging_project_sink.go b/google/resource_logging_project_sink.go index a7a04115bc3..004c811be9c 100644 --- a/google/resource_logging_project_sink.go +++ b/google/resource_logging_project_sink.go @@ -4,53 +4,30 @@ import ( "fmt" "github.com/hashicorp/terraform/helper/schema" - "google.golang.org/api/logging/v2" ) const nonUniqueWriterAccount = "serviceAccount:cloud-logs@system.gserviceaccount.com" func resourceLoggingProjectSink() *schema.Resource { - return &schema.Resource{ + schm := &schema.Resource{ Create: resourceLoggingProjectSinkCreate, Read: resourceLoggingProjectSinkRead, Delete: resourceLoggingProjectSinkDelete, Update: resourceLoggingProjectSinkUpdate, - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - - "destination": { - Type: schema.TypeString, - Required: true, - }, - - "filter": { - Type: schema.TypeString, - Optional: true, - }, - - "project": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "unique_writer_identity": { - Type: schema.TypeBool, - Optional: true, - Default: false, - ForceNew: true, - }, - - "writer_identity": { - Type: schema.TypeString, - Computed: true, - }, - }, + Schema: resourceLoggingSinkSchema(), } + schm.Schema["project"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + } + schm.Schema["unique_writer_identity"] = &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + } + return schm } func resourceLoggingProjectSinkCreate(d *schema.ResourceData, meta interface{}) error { @@ -61,23 +38,10 @@ func resourceLoggingProjectSinkCreate(d *schema.ResourceData, meta interface{}) return err } - name := d.Get("name").(string) - - id := LoggingSinkId{ - resourceType: "projects", - resourceId: project, - name: name, - } - - sink := logging.LogSink{ - Name: d.Get("name").(string), - Destination: d.Get("destination").(string), - Filter: d.Get("filter").(string), - } - + id, sink := expandResourceLoggingSink(d, "projects", project) uniqueWriterIdentity := d.Get("unique_writer_identity").(bool) - _, err = config.clientLogging.Projects.Sinks.Create(id.parent(), &sink).UniqueWriterIdentity(uniqueWriterIdentity).Do() + _, err = config.clientLogging.Projects.Sinks.Create(id.parent(), sink).UniqueWriterIdentity(uniqueWriterIdentity).Do() if err != nil { return err } @@ -95,10 +59,7 @@ func resourceLoggingProjectSinkRead(d *schema.ResourceData, meta interface{}) er return handleNotFoundError(err, d, fmt.Sprintf("Project Logging Sink %s", d.Get("name").(string))) } - d.Set("name", sink.Name) - d.Set("destination", sink.Destination) - d.Set("filter", sink.Filter) - d.Set("writer_identity", sink.WriterIdentity) + flattenResourceLoggingSink(d, sink) if sink.WriterIdentity != nonUniqueWriterAccount { d.Set("unique_writer_identity", true) } else { @@ -110,23 +71,10 @@ func resourceLoggingProjectSinkRead(d *schema.ResourceData, meta interface{}) er func resourceLoggingProjectSinkUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - // Can only update destination/filter right now. Despite the method below using 'Patch', the API requires both - // destination and filter (even if unchanged). - sink := logging.LogSink{ - Destination: d.Get("destination").(string), - Filter: d.Get("filter").(string), - } - - if d.HasChange("destination") { - sink.ForceSendFields = append(sink.ForceSendFields, "Destination") - } - if d.HasChange("filter") { - sink.ForceSendFields = append(sink.ForceSendFields, "Filter") - } - + sink := expandResourceLoggingSinkForUpdate(d) uniqueWriterIdentity := d.Get("unique_writer_identity").(bool) - _, err := config.clientLogging.Projects.Sinks.Patch(d.Id(), &sink).UniqueWriterIdentity(uniqueWriterIdentity).Do() + _, err := config.clientLogging.Projects.Sinks.Patch(d.Id(), sink).UniqueWriterIdentity(uniqueWriterIdentity).Do() if err != nil { return err } diff --git a/google/resource_logging_sink.go b/google/resource_logging_sink.go new file mode 100644 index 00000000000..5789d9e56ab --- /dev/null +++ b/google/resource_logging_sink.go @@ -0,0 +1,70 @@ +package google + +import ( + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/logging/v2" +) + +func resourceLoggingSinkSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "destination": { + Type: schema.TypeString, + Required: true, + }, + + "filter": { + Type: schema.TypeString, + Optional: true, + }, + + "writer_identity": { + Type: schema.TypeString, + Computed: true, + }, + } +} + +func expandResourceLoggingSink(d *schema.ResourceData, resourceType, resourceId string) (LoggingSinkId, *logging.LogSink) { + id := LoggingSinkId{ + resourceType: resourceType, + resourceId: resourceId, + name: d.Get("name").(string), + } + + sink := logging.LogSink{ + Name: d.Get("name").(string), + Destination: d.Get("destination").(string), + Filter: d.Get("filter").(string), + } + return id, &sink +} + +func flattenResourceLoggingSink(d *schema.ResourceData, sink *logging.LogSink) { + d.Set("name", sink.Name) + d.Set("destination", sink.Destination) + d.Set("filter", sink.Filter) + d.Set("writer_identity", sink.WriterIdentity) +} + +func expandResourceLoggingSinkForUpdate(d *schema.ResourceData) *logging.LogSink { + // Can only update destination/filter right now. Despite the method below using 'Patch', the API requires both + // destination and filter (even if unchanged). + sink := logging.LogSink{ + Destination: d.Get("destination").(string), + Filter: d.Get("filter").(string), + } + + if d.HasChange("destination") { + sink.ForceSendFields = append(sink.ForceSendFields, "Destination") + } + if d.HasChange("filter") { + sink.ForceSendFields = append(sink.ForceSendFields, "Filter") + } + return &sink +} diff --git a/website/docs/r/logging_billing_account_sink.html.markdown b/website/docs/r/logging_billing_account_sink.html.markdown new file mode 100644 index 00000000000..5063de7f20f --- /dev/null +++ b/website/docs/r/logging_billing_account_sink.html.markdown @@ -0,0 +1,69 @@ +--- +layout: "google" +page_title: "Google: google_logging_billing-account_sink" +sidebar_current: "docs-google-logging-billing-account-sink" +description: |- + Manages a billing account logging sink. +--- + +# google\_logging\_billing\_account\_sink + +Manages a billing account logging sink. For more information see +[the official documentation](https://cloud.google.com/logging/docs/) and +[Exporting Logs in the API](https://cloud.google.com/logging/docs/api/tasks/exporting-logs). + +Note that you must have the "Logs Configuration Writer" IAM role (`roles/logging.configWriter`) +granted to the credentials used with terraform. + +## Example Usage + +```hcl +resource "google_logging_billing_account_sink" "my-sink" { + name = "my-sink" + billing_account = "ABCDEF-012345-GHIJKL" + + # Can export to pubsub, cloud storage, or bigtable + destination = "storage.googleapis.com/${google_storage_bucket.log-bucket.name}" +} + +resource "google_storage_bucket" "log-bucket" { + name = "billing-logging-bucket" +} + +resource "google_project_iam_binding" "log-writer" { + role = "roles/storage.objectCreator" + + members = [ + "${google_logging_billing_account_sink.my-sink.writer_identity}", + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the logging sink. + +* `billing_account` - (Required) The billing account exported to the sink. + +* `destination` - (Required) The destination of the sink (or, in other words, where logs are written to). Can be a + Cloud Storage bucket, a PubSub topic, or a BigQuery dataset. Examples: +``` +"storage.googleapis.com/[GCS_BUCKET]" +"bigquery.googleapis.com/projects/[PROJECT_ID]/datasets/[DATASET]" +"pubsub.googleapis.com/projects/[PROJECT_ID]/topics/[TOPIC_ID]" +``` + The writer associated with the sink must have access to write to the above resource. + +* `filter` - (Optional) The filter to apply when exporting logs. Only log entries that match the filter are exported. + See (Advanced Log Filters)[https://cloud.google.com/logging/docs/view/advanced_filters] for information on how to + write a filter. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `writer_identity` - The identity associated with this sink. This identity must be granted write access to the + configured `destination`. diff --git a/website/docs/r/logging_project_sink.html.markdown b/website/docs/r/logging_project_sink.html.markdown index 653834a4a10..7f2f80bf1dd 100644 --- a/website/docs/r/logging_project_sink.html.markdown +++ b/website/docs/r/logging_project_sink.html.markdown @@ -59,7 +59,7 @@ resource "google_compute_instance" "my-logged-instance" { } } -# A bucket to storage logs in. +# A bucket to store logs in resource "google_storage_bucket" "log-bucket" { name = "my-unique-logging-bucket" } diff --git a/website/google.erb b/website/google.erb index e09fc5a87db..7350c554763 100644 --- a/website/google.erb +++ b/website/google.erb @@ -342,6 +342,10 @@ > Google Stackdriver Logging Resources