Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add google_logging_organization_sink resource #923

Merged
merged 2 commits into from
Jan 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
90 changes: 90 additions & 0 deletions google/resource_logging_organization_sink.go
Original file line number Diff line number Diff line change
@@ -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
}
180 changes: 180 additions & 0 deletions google/resource_logging_organization_sink_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
75 changes: 75 additions & 0 deletions website/docs/r/logging_organization_sink.hml.markdown
Original file line number Diff line number Diff line change
@@ -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`.
4 changes: 4 additions & 0 deletions website/google.erb
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@
<a href="/docs/providers/google/r/logging_billing_account_sink.html">google_logging_billing_account_sink</a>
</li>

<li<%= sidebar_current("docs-google-logging-organization-sink") %>>
<a href="/docs/providers/google/r/logging_organization_sink.html">google_logging_organization_sink</a>
</li>

<li<%= sidebar_current("docs-google-logging-folder-sink") %>>
<a href="/docs/providers/google/r/logging_folder_sink.html">google_logging_folder_sink</a>
</li>
Expand Down