From f214bd29633a15af39f4a17e1c966106be51d3ac Mon Sep 17 00:00:00 2001 From: Alexander Makarenko Date: Sat, 4 Sep 2021 22:18:44 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Log=20Vie?= =?UTF-8?q?ws?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `google_logging_view` resource was added --- google/provider.go | 1 + google/resource_logging_view.go | 231 ++++++++++++++++ google/resource_logging_view_test.go | 249 ++++++++++++++++++ .../docs/r/logging_project_view.html.markdown | 63 +++++ 4 files changed, 544 insertions(+) create mode 100644 google/resource_logging_view.go create mode 100644 google/resource_logging_view_test.go create mode 100644 website/docs/r/logging_project_view.html.markdown diff --git a/google/provider.go b/google/provider.go index 9cf9dc9661d..5deaec3c030 100644 --- a/google/provider.go +++ b/google/provider.go @@ -1171,6 +1171,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_logging_project_sink": resourceLoggingProjectSink(), "google_logging_project_exclusion": ResourceLoggingExclusion(ProjectLoggingExclusionSchema, NewProjectLoggingExclusionUpdater, projectLoggingExclusionIdParseFunc), "google_logging_project_bucket_config": ResourceLoggingProjectBucketConfig(), + "google_logging_view": ResourceLoggingView(), "google_monitoring_dashboard": resourceMonitoringDashboard(), "google_service_networking_connection": resourceServiceNetworkingConnection(), "google_sql_database_instance": resourceSqlDatabaseInstance(), diff --git a/google/resource_logging_view.go b/google/resource_logging_view.go new file mode 100644 index 00000000000..a9ce17731e9 --- /dev/null +++ b/google/resource_logging_view.go @@ -0,0 +1,231 @@ +package google + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var loggingViewResourceTypes = []string{ + "billingAccounts", + "folders", + "organizations", + "projects", +} + +var loggingViewSchema = map[string]*schema.Schema{ + "view_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The ID of the view", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `The resource name of the view`, + }, + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The resource name of the bucket.`, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `An optional description for this bucket.`, + }, + "filter": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: `Filter that restricts which log entries in a bucket are visible in this view.`, + }, +} + +func ResourceLoggingView() *schema.Resource { + return &schema.Resource{ + Create: resourceLoggingViewCreate, + Read: resourceLoggingViewRead, + Update: resourceLoggingViewUpdate, + Delete: resourceLoggingViewDelete, + Importer: &schema.ResourceImporter{ + State: resourceLoggingViewImportState, + }, + Schema: loggingViewSchema, + UseJSONNumber: true, + } +} + +var loggingViewIDRegex = regexp.MustCompile("((.+)/.+/locations/.+/buckets/.+)/views/(.+)") + +func resourceLoggingViewImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + parts := loggingViewIDRegex.FindStringSubmatch(d.Id()) + if parts == nil { + return nil, fmt.Errorf("Unable to parse Log View id %#v", d.Id()) + } + + if len(parts) != 4 { + return nil, fmt.Errorf("Invalid id format. Format should be '{{parent}}/{{parent_id}}/locations/{{location}}/buckets/{{bucket_id}}/views/{{view_id}} with parent in %s", loggingSinkResourceTypes) + } + + validLoggingType := false + for _, v := range loggingViewResourceTypes { + if v == parts[2] { + validLoggingType = true + break + } + } + if !validLoggingType { + return nil, fmt.Errorf("Logging parent type %s is not valid. Valid resource types: %#v", parts[1], + loggingViewResourceTypes) + } + + if err := d.Set("bucket", parts[1]); err != nil { + return nil, fmt.Errorf("Error setting bucket: %s", err) + } + if err := d.Set("view_id", parts[3]); err != nil { + return nil, fmt.Errorf("Error setting view_id: %s", err) + } + + return []*schema.ResourceData{d}, nil +} + +func resourceLoggingViewCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + obj["filter"] = d.Get("filter") + obj["description"] = d.Get("description") + + url, err := replaceVars(d, config, "{{LoggingBasePath}}{{bucket}}/views?viewId={{view_id}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Log View: %#v", obj) + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return err + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "POST", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Log View: %s", err) + } + + d.SetId(fmt.Sprintf("%s/views/%s", d.Get("bucket"), d.Get("view_id"))) + log.Printf("[DEBUG] Finished creating Log View %q: %#v", d.Id(), res) + + return resourceLoggingViewRead(d, meta) +} + +func resourceLoggingViewRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + log.Printf("[DEBUG] Fetching Log View: %#v", d.Id()) + + url, err := replaceVars(d, config, fmt.Sprintf("{{LoggingBasePath}}%s", d.Id())) + if err != nil { + return err + } + + res, err := sendRequest(config, "GET", "", url, userAgent, nil) + if err != nil { + log.Printf("[WARN] Unable to acquire Log View at %s", d.Id()) + + d.SetId("") + return err + } + + if err := d.Set("name", res["name"]); err != nil { + return fmt.Errorf("Error setting name: %s", err) + } + if err := d.Set("filter", res["filter"]); err != nil { + return fmt.Errorf("Error setting filter: %s", err) + } + if err := d.Set("description", res["description"]); err != nil { + return fmt.Errorf("Error setting description: %s", err) + } + + return nil +} + +func resourceLoggingViewUpdate(d *schema.ResourceData, meta interface{}) error { + var updateMask []string + if d.HasChange("filter") { + updateMask = append(updateMask, "filter") + } + if d.HasChange("description") { + updateMask = append(updateMask, "description") + } + return resourceLoggingViewUpdateWithUpdateMask(d, meta, updateMask) +} + +func resourceLoggingViewUpdateWithUpdateMask(d *schema.ResourceData, meta interface{}, updateMask []string) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + for _, field := range updateMask { + obj[field] = d.Get(field) + } + + url, err := replaceVars(d, config, fmt.Sprintf("{{LoggingBasePath}}%s", d.Id())) + if err != nil { + return err + } + + url, err = addQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + _, err = sendRequestWithTimeout(config, "PATCH", "", url, userAgent, obj, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return fmt.Errorf("Error updating Log View %q: %s", d.Id(), err) + } + + return resourceLoggingViewRead(d, meta) +} + +func resourceLoggingViewDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + + url, err := replaceVars(d, config, fmt.Sprintf("{{LoggingBasePath}}%s", d.Id())) + if err != nil { + return err + } + + _, err = sendRequest(config, "DELETE", "", url, userAgent, nil) + if err != nil { + return handleNotFoundError(err, d, "Log View") + } + + d.SetId("") + return nil +} diff --git a/google/resource_logging_view_test.go b/google/resource_logging_view_test.go new file mode 100644 index 00000000000..9d1eacb4a96 --- /dev/null +++ b/google/resource_logging_view_test.go @@ -0,0 +1,249 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccLoggingView_basic(t *testing.T) { + t.Parallel() + + projectId := getTestProjectFromEnv() + bucketId := fmt.Sprintf("projects/%s/locations/global/buckets/_Default", projectId) + viewId := "tf-test-view-" + randString(t, 10) + notTestProjectFilter := fmt.Sprintf("NOT source(projects/%s)", getTestProjectFromEnv()) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLogViewDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccLoggingView_basic(bucketId, viewId, "All logs", ""), + }, + { + ResourceName: "google_logging_view.basic", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccLoggingView_basic(bucketId, viewId, "All logs (except test project)", notTestProjectFilter), + }, + { + ResourceName: "google_logging_view.basic", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccLoggingView_basic(bucketId, viewId, "All logs", ""), + }, + { + ResourceName: "google_logging_view.basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccLoggingView_billingAccount(t *testing.T) { + t.Parallel() + + billingAccountId := getTestBillingAccountFromEnv(t) + bucketId := fmt.Sprintf("billingAccounts/%s/locations/global/buckets/_Default", billingAccountId) + viewId := "tf-test-view-" + randString(t, 10) + notTestProjectFilter := fmt.Sprintf("NOT source(projects/%s)", getTestProjectFromEnv()) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLogViewDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccLoggingView_basic(bucketId, viewId, "All logs", ""), + }, + { + ResourceName: "google_logging_view.basic", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccLoggingView_basic(bucketId, viewId, "All logs (except test project)", notTestProjectFilter), + }, + { + ResourceName: "google_logging_view.basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccLoggingView_organization(t *testing.T) { + t.Parallel() + + organizationId := getTestOrgFromEnv(t) + bucketId := fmt.Sprintf("organizations/%s/locations/global/buckets/_Default", organizationId) + viewId := "tf-test-view-" + randString(t, 10) + notTestProjectFilter := fmt.Sprintf("NOT source(projects/%s)", getTestProjectFromEnv()) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLogViewDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccLoggingView_basic(bucketId, viewId, "All logs", ""), + }, + { + ResourceName: "google_logging_view.basic", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccLoggingView_basic(bucketId, viewId, "All logs (except test project)", notTestProjectFilter), + }, + { + ResourceName: "google_logging_view.basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccLoggingView_folder(t *testing.T) { + t.Parallel() + + organizationId := getTestOrgFromEnv(t) + folderName := "tf-test-folder-" + randString(t, 10) + viewId := "tf-test-view-" + randString(t, 10) + notTestProjectFilter := fmt.Sprintf("NOT source(projects/%s)", getTestProjectFromEnv()) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLogViewDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccLoggingView_folder(organizationId, folderName, viewId, "All logs", ""), + }, + { + ResourceName: "google_logging_view.folder", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccLoggingView_folder(organizationId, folderName, viewId, "All logs (except test project)", notTestProjectFilter), + }, + { + ResourceName: "google_logging_view.folder", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccLoggingView_customBucket(t *testing.T) { + t.Parallel() + + projectId := getTestProjectFromEnv() + bucketName := "tf-test-bucket-" + randString(t, 10) + viewId := "tf-test-view-" + randString(t, 10) + notTestProjectFilter := fmt.Sprintf("NOT source(projects/%s)", getTestProjectFromEnv()) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLogViewDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccLoggingView_customBucket(projectId, bucketName, viewId, "All logs", ""), + }, + { + ResourceName: "google_logging_view.custom_bucket_view", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccLoggingView_customBucket(projectId, bucketName, viewId, "All logs (except test project)", notTestProjectFilter), + }, + { + ResourceName: "google_logging_view.custom_bucket_view", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckLogViewDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + config := googleProviderConfig(t) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_logging_view" { + continue + } + + attributes := rs.Primary.Attributes + + _, err := config.NewLoggingClient(config.userAgent).Locations.Buckets.Views.Get(attributes["id"]).Do() + if err == nil { + return fmt.Errorf("Log View still exists") + } + } + + return nil + } +} + +func testAccLoggingView_basic(bucketId, viewId, description, filter string) string { + return fmt.Sprintf(` +resource "google_logging_view" "basic" { + bucket = "%s" + view_id = "%s" + description = "%s" + filter = "%s" +} +`, bucketId, viewId, description, filter) +} + +func testAccLoggingView_folder(organizationId, folderName, viewId, description, filter string) string { + return fmt.Sprintf(` +resource "google_folder" "test_folder" { + parent = "organizations/%s" + display_name = "%s" +} + +resource "google_logging_view" "folder" { + view_id = "%s" + bucket = "${google_folder.test_folder.name}/locations/global/buckets/_Default" + description = "%s" + filter = "%s" +} +`, organizationId, folderName, viewId, description, filter) +} + +func testAccLoggingView_customBucket(projectId, bucketName, viewId, description, filter string) string { + return fmt.Sprintf(` +resource "google_logging_project_bucket_config" "custom_bucket" { + project = "%s" + bucket_id = "%s" + location = "us-west1" + retention_days = 30 + description = "Log View test" +} + +resource "google_logging_view" "custom_bucket_view" { + view_id = "%s" + bucket = google_logging_project_bucket_config.custom_bucket.id + description = "%s" + filter = "%s" +} +`, projectId, bucketName, viewId, description, filter) +} diff --git a/website/docs/r/logging_project_view.html.markdown b/website/docs/r/logging_project_view.html.markdown new file mode 100644 index 00000000000..65224e3df4e --- /dev/null +++ b/website/docs/r/logging_project_view.html.markdown @@ -0,0 +1,63 @@ +--- +subcategory: "Cloud (Stackdriver) Logging" +layout: "google" +page_title: "Google: google_logging_view" +sidebar_current: "docs-google-logging-view" +description: |- + Manages a log bucket view. +--- + +# google\_logging\_view + +Manages a log bucket view. For more information see +[the official logging documentation](https://cloud.google.com/logging/docs/) and +[Managing log views on your log buckets](https://cloud.google.com/logging/docs/logs-views). + +~> **Note:** Log views are automatically created for a log bucket. Creating a resource of this type will fail in Terraform. The log views that are currently automatically created are `_AllLogs` and `_Default`. + +## Example Usage + +```hcl +resource "google_logging_project_bucket_config" "default" { + bucket_id = "_Default" + project = google_project.default.name + location = "global" + retention_days = 30 +} + +resource "google_logging_view" "only_compute_instances" { + view_id = "OnlyComputeInstances" + bucket = google_logging_project_bucket_config.default.id + description = "Compute instance logs" + filter = "resource.type = gce_instance" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `view_id` - (Required) Log view identifier. + +* `bucket` - (Required) Resource name of the log bucket with the following format: `{{parent}}/{{parent_id}}/locations/{{location}}/buckets/{{bucket_id}}`. + +* `description` - (Optional) Description of this log view. + +* `filter` - (Optional) Log view filter. Read more about filter constraints in the [official documentation](https://cloud.google.com/logging/docs/logs-views#before_you_begin). + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `id` - An identifier for the resource with format `{{parent}}/{{parent_id}}/locations/{{location}}/buckets/{{bucket_id}}/views/{{view_id}}`. + +* `name` - The resource name of the log view. For example: `{{parent}}/{{parent_id}}/locations/{{location}}/buckets/{{bucket_id}}/views/{{view_id}}`. + +## Import + +This resource can be imported using the following format: + +``` +$ terraform import google_logging_view.basic {{parent}}/{{parent_id}}/locations/{{location}}/buckets/{{bucket_id}}/views/{{view_id}} +``` From dad3c8c96b47e024e88cca6035c4811d5b8a3fec Mon Sep 17 00:00:00 2001 From: Alexander Makarenko Date: Tue, 7 Sep 2021 21:58:31 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20Fix=20documentation=20file?= =?UTF-8?q?=20ending?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._project_view.html.markdown => logging_view.html.markdown} | 0 website/google.erb | 4 ++++ 2 files changed, 4 insertions(+) rename website/docs/r/{logging_project_view.html.markdown => logging_view.html.markdown} (100%) diff --git a/website/docs/r/logging_project_view.html.markdown b/website/docs/r/logging_view.html.markdown similarity index 100% rename from website/docs/r/logging_project_view.html.markdown rename to website/docs/r/logging_view.html.markdown diff --git a/website/google.erb b/website/google.erb index e4e613c2373..fa8b7f269b3 100644 --- a/website/google.erb +++ b/website/google.erb @@ -482,6 +482,10 @@ google_logging_project_sink +
  • + google_logging_view +
  • +