diff --git a/google/provider.go b/google/provider.go index cb091ea6ff1..871d538c410 100644 --- a/google/provider.go +++ b/google/provider.go @@ -137,6 +137,7 @@ func Provider() terraform.ResourceProvider { "google_folder_iam_policy": ResourceIamPolicyWithImport(IamFolderSchema, NewFolderIamUpdater, FolderIdParseFunc), "google_folder_organization_policy": resourceGoogleFolderOrganizationPolicy(), "google_logging_billing_account_sink": resourceLoggingBillingAccountSink(), + "google_logging_organization_sink": resourceLoggingOrganizationSink(), "google_logging_folder_sink": resourceLoggingFolderSink(), "google_logging_project_sink": resourceLoggingProjectSink(), "google_kms_key_ring": resourceKmsKeyRing(), diff --git a/google/resource_logging_organization_sink.go b/google/resource_logging_organization_sink.go new file mode 100644 index 00000000000..a1660ed57b3 --- /dev/null +++ b/google/resource_logging_organization_sink.go @@ -0,0 +1,90 @@ +package google + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceLoggingOrganizationSink() *schema.Resource { + schm := &schema.Resource{ + Create: resourceLoggingOrganizationSinkCreate, + Read: resourceLoggingOrganizationSinkRead, + Delete: resourceLoggingOrganizationSinkDelete, + Update: resourceLoggingOrganizationSinkUpdate, + Schema: resourceLoggingSinkSchema(), + } + schm.Schema["org_id"] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: optionalPrefixSuppress("organizations/"), + } + schm.Schema["include_children"] = &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: false, + } + + return schm +} + +func resourceLoggingOrganizationSinkCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + org := d.Get("org_id").(string) + id, sink := expandResourceLoggingSink(d, "organizations", org) + sink.IncludeChildren = d.Get("include_children").(bool) + + // Must use a unique writer, since all destinations are in projects. + // The API will reject any requests that don't explicitly set 'uniqueWriterIdentity' to true. + _, err := config.clientLogging.Organizations.Sinks.Create(id.parent(), sink).UniqueWriterIdentity(true).Do() + if err != nil { + return err + } + + d.SetId(id.canonicalId()) + return resourceLoggingOrganizationSinkRead(d, meta) +} + +func resourceLoggingOrganizationSinkRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + sink, err := config.clientLogging.Organizations.Sinks.Get(d.Id()).Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Organization Logging Sink %s", d.Get("name").(string))) + } + + flattenResourceLoggingSink(d, sink) + d.Set("include_children", sink.IncludeChildren) + + return nil +} + +func resourceLoggingOrganizationSinkUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + sink := expandResourceLoggingSinkForUpdate(d) + // It seems the API might actually accept an update for include_children; this is not in the list of updatable + // properties though and might break in the future. Always include the value to prevent it changing. + sink.IncludeChildren = d.Get("include_children").(bool) + sink.ForceSendFields = append(sink.ForceSendFields, "IncludeChildren") + + // The API will reject any requests that don't explicitly set 'uniqueWriterIdentity' to true. + _, err := config.clientLogging.Organizations.Sinks.Patch(d.Id(), sink).UniqueWriterIdentity(true).Do() + if err != nil { + return err + } + + return resourceLoggingOrganizationSinkRead(d, meta) +} + +func resourceLoggingOrganizationSinkDelete(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_organization_sink_test.go b/google/resource_logging_organization_sink_test.go new file mode 100644 index 00000000000..ae1c92b49b3 --- /dev/null +++ b/google/resource_logging_organization_sink_test.go @@ -0,0 +1,180 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/logging/v2" + "strconv" +) + +func TestAccLoggingOrganizationSink_basic(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + sinkName := "tf-test-sink-" + acctest.RandString(10) + bucketName := "tf-test-sink-bucket-" + acctest.RandString(10) + + var sink logging.LogSink + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLoggingOrganizationSinkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccLoggingOrganizationSink_basic(sinkName, bucketName, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckLoggingOrganizationSinkExists("google_logging_organization_sink.basic", &sink), + testAccCheckLoggingOrganizationSink(&sink, "google_logging_organization_sink.basic"), + ), + }, + }, + }) +} + +func TestAccLoggingOrganizationSink_update(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + sinkName := "tf-test-sink-" + acctest.RandString(10) + bucketName := "tf-test-sink-bucket-" + acctest.RandString(10) + updatedBucketName := "tf-test-sink-bucket-" + acctest.RandString(10) + + var sinkBefore, sinkAfter logging.LogSink + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLoggingOrganizationSinkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccLoggingOrganizationSink_update(sinkName, bucketName, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckLoggingOrganizationSinkExists("google_logging_organization_sink.update", &sinkBefore), + testAccCheckLoggingOrganizationSink(&sinkBefore, "google_logging_organization_sink.update"), + ), + }, { + Config: testAccLoggingOrganizationSink_update(sinkName, updatedBucketName, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckLoggingOrganizationSinkExists("google_logging_organization_sink.update", &sinkAfter), + testAccCheckLoggingOrganizationSink(&sinkAfter, "google_logging_organization_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 testAccCheckLoggingOrganizationSinkDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_logging_organization_sink" { + continue + } + + attributes := rs.Primary.Attributes + + _, err := config.clientLogging.Organizations.Sinks.Get(attributes["id"]).Do() + if err == nil { + return fmt.Errorf("organization sink still exists") + } + } + + return nil +} + +func testAccCheckLoggingOrganizationSinkExists(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.Organizations.Sinks.Get(attributes["id"]).Do() + if err != nil { + return err + } + *sink = *si + + return nil + } +} + +func testAccCheckLoggingOrganizationSink(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"]) + } + + includeChildren := false + if attributes["include_children"] != "" { + includeChildren, err = strconv.ParseBool(attributes["include_children"]) + if err != nil { + return err + } + } + if sink.IncludeChildren != includeChildren { + return fmt.Errorf("mismatch on include_children: api has %v but client has %v", sink.IncludeChildren, includeChildren) + } + + return nil + } +} + +func testAccLoggingOrganizationSink_basic(sinkName, bucketName, orgId string) string { + return fmt.Sprintf(` +resource "google_logging_organization_sink" "basic" { + name = "%s" + org_id = "%s" + destination = "storage.googleapis.com/${google_storage_bucket.log-bucket.name}" + filter = "logName=\"projects/%s/logs/compute.googleapis.com%%2Factivity_log\" AND severity>=ERROR" + include_children = true +} + +resource "google_storage_bucket" "log-bucket" { + name = "%s" +}`, sinkName, orgId, getTestProjectFromEnv(), bucketName) +} + +func testAccLoggingOrganizationSink_update(sinkName, bucketName, orgId string) string { + return fmt.Sprintf(` +resource "google_logging_organization_sink" "update" { + name = "%s" + org_id = "%s" + destination = "storage.googleapis.com/${google_storage_bucket.log-bucket.name}" + filter = "logName=\"projects/%s/logs/compute.googleapis.com%%2Factivity_log\" AND severity>=ERROR" + destination = "storage.googleapis.com/${google_storage_bucket.log-bucket.name}" + include_children = false +} + +resource "google_storage_bucket" "log-bucket" { + name = "%s" +}`, sinkName, orgId, getTestProjectFromEnv(), bucketName) +} diff --git a/website/docs/r/logging_organization_sink.hml.markdown b/website/docs/r/logging_organization_sink.hml.markdown new file mode 100644 index 00000000000..2d6a6a43cdc --- /dev/null +++ b/website/docs/r/logging_organization_sink.hml.markdown @@ -0,0 +1,75 @@ +--- +layout: "google" +page_title: "Google: google_logging_organization_sink" +sidebar_current: "docs-google-logging-organization-sink" +description: |- + Manages a organization-level logging sink. +--- + +# google\_logging\_organization\_sink + +Manages a organization-level 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_organization_sink" "my-sink" { + name = "my-sink" + org_id = "123456789" + + # Can export to pubsub, cloud storage, or bigtable + destination = "storage.googleapis.com/${google_storage_bucket.log-bucket.name}" + + # Log all WARN or higher severity messages relating to instances + filter = "resource.type = gce_instance AND severity >= WARN" +} + +resource "google_storage_bucket" "log-bucket" { + name = "organization-logging-bucket" +} + +resource "google_project_iam_binding" "log-writer" { + role = "roles/storage.objectCreator" + + members = [ + "${google_logging_organization_sink.my-sink.writer_identity}", + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the logging sink. + +* `org_id` - (Required) The numeric ID of the organization to be 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. + +* `include_children` - (Optional) Whether or not to include children organizations in the sink export. If true, logs + associated with child projects are also exported; otherwise only logs relating to the provided organization are included. + +## 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/google.erb b/website/google.erb index 1e2009321dc..5dddbe9e06f 100644 --- a/website/google.erb +++ b/website/google.erb @@ -448,6 +448,10 @@ google_logging_billing_account_sink + > + google_logging_organization_sink + + > google_logging_folder_sink