From 71a6b8f7126e92ab1f72183688ee60554d948819 Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Wed, 20 May 2020 01:29:01 +0000 Subject: [PATCH] stackdriver dashboards resource (#3490) Signed-off-by: Modular Magician --- .changelog/3490.txt | 6 + google/config.go | 2 +- google/provider.go | 1 + google/resource_monitoring_alert_policy.go | 8 +- google/resource_monitoring_dashboard.go | 193 +++++++++++++ google/resource_monitoring_dashboard_test.go | 271 ++++++++++++++++++ google/resource_monitoring_group.go | 8 +- ...esource_monitoring_group_generated_test.go | 2 +- ...esource_monitoring_notification_channel.go | 8 +- ...ing_notification_channel_generated_test.go | 2 +- google/resource_monitoring_service.go | 8 +- ...ource_monitoring_service_generated_test.go | 2 +- google/resource_monitoring_slo.go | 8 +- .../resource_monitoring_slo_generated_test.go | 2 +- ...resource_monitoring_uptime_check_config.go | 8 +- ...ring_uptime_check_config_generated_test.go | 2 +- .../guides/provider_reference.html.markdown | 2 +- .../docs/r/monitoring_dashboard.html.markdown | 156 ++++++++++ website/google.erb | 4 + 19 files changed, 662 insertions(+), 31 deletions(-) create mode 100644 .changelog/3490.txt create mode 100644 google/resource_monitoring_dashboard.go create mode 100644 google/resource_monitoring_dashboard_test.go create mode 100644 website/docs/r/monitoring_dashboard.html.markdown diff --git a/.changelog/3490.txt b/.changelog/3490.txt new file mode 100644 index 00000000000..8d7ef99827e --- /dev/null +++ b/.changelog/3490.txt @@ -0,0 +1,6 @@ +```release-note:new-resource +`google_monitoring_dashboard` +``` +```release-note:breaking-change +The base url for the `monitoring` endpoint no longer includes the API version (previously "v3/"). If you use a `monitoring_custom_endpoint`, remove the trailing "v3/". +``` diff --git a/google/config.go b/google/config.go index 6250220563f..d1bcad54695 100644 --- a/google/config.go +++ b/google/config.go @@ -243,7 +243,7 @@ var IdentityPlatformDefaultBasePath = "https://identitytoolkit.googleapis.com/v2 var KMSDefaultBasePath = "https://cloudkms.googleapis.com/v1/" var LoggingDefaultBasePath = "https://logging.googleapis.com/v2/" var MLEngineDefaultBasePath = "https://ml.googleapis.com/v1/" -var MonitoringDefaultBasePath = "https://monitoring.googleapis.com/v3/" +var MonitoringDefaultBasePath = "https://monitoring.googleapis.com/" var OSLoginDefaultBasePath = "https://oslogin.googleapis.com/v1/" var PubsubDefaultBasePath = "https://pubsub.googleapis.com/v1/" var RedisDefaultBasePath = "https://redis.googleapis.com/v1/" diff --git a/google/provider.go b/google/provider.go index a149a9664d8..cf629f2fa97 100644 --- a/google/provider.go +++ b/google/provider.go @@ -808,6 +808,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_kms_crypto_key_iam_binding": ResourceIamBinding(IamKmsCryptoKeySchema, NewKmsCryptoKeyIamUpdater, CryptoIdParseFunc), "google_kms_crypto_key_iam_member": ResourceIamMember(IamKmsCryptoKeySchema, NewKmsCryptoKeyIamUpdater, CryptoIdParseFunc), "google_kms_crypto_key_iam_policy": ResourceIamPolicy(IamKmsCryptoKeySchema, NewKmsCryptoKeyIamUpdater, CryptoIdParseFunc), + "google_monitoring_dashboard": resourceMonitoringDashboard(), "google_service_networking_connection": resourceServiceNetworkingConnection(), "google_spanner_instance_iam_binding": ResourceIamBinding(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc), "google_spanner_instance_iam_member": ResourceIamMember(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc), diff --git a/google/resource_monitoring_alert_policy.go b/google/resource_monitoring_alert_policy.go index 7e21a88c883..cdc8102060c 100644 --- a/google/resource_monitoring_alert_policy.go +++ b/google/resource_monitoring_alert_policy.go @@ -775,7 +775,7 @@ func resourceMonitoringAlertPolicyCreate(d *schema.ResourceData, meta interface{ mutexKV.Lock(lockName) defer mutexKV.Unlock(lockName) - url, err := replaceVars(d, config, "{{MonitoringBasePath}}projects/{{project}}/alertPolicies") + url, err := replaceVars(d, config, "{{MonitoringBasePath}}v3/projects/{{project}}/alertPolicies") if err != nil { return err } @@ -816,7 +816,7 @@ func resourceMonitoringAlertPolicyCreate(d *schema.ResourceData, meta interface{ func resourceMonitoringAlertPolicyRead(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - url, err := replaceVars(d, config, "{{MonitoringBasePath}}{{name}}") + url, err := replaceVars(d, config, "{{MonitoringBasePath}}v3/{{name}}") if err != nil { return err } @@ -936,7 +936,7 @@ func resourceMonitoringAlertPolicyUpdate(d *schema.ResourceData, meta interface{ mutexKV.Lock(lockName) defer mutexKV.Unlock(lockName) - url, err := replaceVars(d, config, "{{MonitoringBasePath}}{{name}}") + url, err := replaceVars(d, config, "{{MonitoringBasePath}}v3/{{name}}") if err != nil { return err } @@ -1001,7 +1001,7 @@ func resourceMonitoringAlertPolicyDelete(d *schema.ResourceData, meta interface{ mutexKV.Lock(lockName) defer mutexKV.Unlock(lockName) - url, err := replaceVars(d, config, "{{MonitoringBasePath}}{{name}}") + url, err := replaceVars(d, config, "{{MonitoringBasePath}}v3/{{name}}") if err != nil { return err } diff --git a/google/resource_monitoring_dashboard.go b/google/resource_monitoring_dashboard.go new file mode 100644 index 00000000000..a32422f9525 --- /dev/null +++ b/google/resource_monitoring_dashboard.go @@ -0,0 +1,193 @@ +package google + +import ( + "fmt" + "reflect" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/structure" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func monitoringDashboardDiffSuppress(k, old, new string, d *schema.ResourceData) bool { + computedFields := []string{"etag", "name"} + + oldMap, err := structure.ExpandJsonFromString(old) + if err != nil { + return false + } + + newMap, err := structure.ExpandJsonFromString(new) + if err != nil { + return false + } + + for _, f := range computedFields { + delete(oldMap, f) + delete(newMap, f) + } + + return reflect.DeepEqual(oldMap, newMap) +} + +func resourceMonitoringDashboard() *schema.Resource { + return &schema.Resource{ + Create: resourceMonitoringDashboardCreate, + Read: resourceMonitoringDashboardRead, + Update: resourceMonitoringDashboardUpdate, + Delete: resourceMonitoringDashboardDelete, + + Importer: &schema.ResourceImporter{ + State: resourceMonitoringDashboardImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(4 * time.Minute), + Update: schema.DefaultTimeout(4 * time.Minute), + Delete: schema.DefaultTimeout(4 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "dashboard_json": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.ValidateJsonString, + DiffSuppressFunc: monitoringDashboardDiffSuppress, + StateFunc: func(v interface{}) string { + json, _ := structure.NormalizeJsonString(v) + return json + }, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func resourceMonitoringDashboardCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + obj, err := structure.ExpandJsonFromString(d.Get("dashboard_json").(string)) + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{MonitoringBasePath}}v1/projects/{{project}}/dashboards") + if err != nil { + return err + } + res, err := sendRequestWithTimeout(config, "POST", project, url, obj, d.Timeout(schema.TimeoutCreate), isMonitoringRetryableError) + if err != nil { + return fmt.Errorf("Error creating Dashboard: %s", err) + } + + name, ok := res["name"] + if !ok { + return fmt.Errorf("Create response didn't contain critical fields. Create may not have succeeded.") + } + d.SetId(name.(string)) + + return resourceMonitoringDashboardRead(d, config) +} + +func resourceMonitoringDashboardRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + url := config.MonitoringBasePath + "v1/" + d.Id() + + project, err := getProject(d, config) + if err != nil { + return err + } + + res, err := sendRequest(config, "GET", project, url, nil, isMonitoringRetryableError) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("MonitoringDashboard %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading Dashboard: %s", err) + } + + str, err := structure.FlattenJsonToString(res) + if err != nil { + return fmt.Errorf("Error reading Dashboard: %s", err) + } + if err = d.Set("dashboard_json", str); err != nil { + return fmt.Errorf("Error reading Dashboard: %s", err) + } + + return nil +} + +func resourceMonitoringDashboardUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + o, n := d.GetChange("dashboard_json") + oObj, err := structure.ExpandJsonFromString(o.(string)) + if err != nil { + return err + } + nObj, err := structure.ExpandJsonFromString(n.(string)) + if err != nil { + return err + } + + nObj["etag"] = oObj["etag"] + + project, err := getProject(d, config) + if err != nil { + return err + } + + url := config.MonitoringBasePath + "v1/" + d.Id() + _, err = sendRequestWithTimeout(config, "PATCH", project, url, nObj, d.Timeout(schema.TimeoutUpdate), isMonitoringRetryableError) + if err != nil { + return fmt.Errorf("Error updating Dashboard %q: %s", d.Id(), err) + } + + return resourceMonitoringDashboardRead(d, config) +} + +func resourceMonitoringDashboardDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + url := config.MonitoringBasePath + "v1/" + d.Id() + + project, err := getProject(d, config) + if err != nil { + return err + } + + _, err = sendRequestWithTimeout(config, "DELETE", project, url, nil, d.Timeout(schema.TimeoutDelete), isMonitoringRetryableError) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("MonitoringDashboard %q", d.Id())) + } + + return nil +} + +func resourceMonitoringDashboardImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + + // current import_formats can't import fields with forward slashes in their value + parts, err := getImportIdQualifiers([]string{"projects/(?P[^/]+)/dashboards/(?P[^/]+)", "(?P[^/]+)"}, d, config, d.Id()) + if err != nil { + return nil, err + } + + d.Set("project", parts["project"]) + d.SetId(fmt.Sprintf("projects/%s/dashboards/%s", parts["project"], parts["id"])) + + return []*schema.ResourceData{d}, nil +} diff --git a/google/resource_monitoring_dashboard_test.go b/google/resource_monitoring_dashboard_test.go new file mode 100644 index 00000000000..55cc74ff84f --- /dev/null +++ b/google/resource_monitoring_dashboard_test.go @@ -0,0 +1,271 @@ +package google + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccMonitoringDashboard_basic(t *testing.T) { + t.Parallel() + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckMonitoringDashboardDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccMonitoringDashboard_basic(), + }, + { + ResourceName: "google_monitoring_dashboard.dashboard", + ImportState: true, + ImportStateVerify: true, + // Default import format uses the ID, which contains the project # + // Testing import formats with the project name don't work because we set + // the ID on import to what the user specified, which won't match the ID + // from the apply + ImportStateVerifyIgnore: []string{"project"}, + }, + }, + }) +} + +func TestAccMonitoringDashboard_gridLayout(t *testing.T) { + t.Parallel() + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckMonitoringDashboardDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccMonitoringDashboard_gridLayout(), + }, + { + ResourceName: "google_monitoring_dashboard.dashboard", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"project"}, + }, + }, + }) +} + +func TestAccMonitoringDashboard_rowLayout(t *testing.T) { + t.Parallel() + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckMonitoringDashboardDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccMonitoringDashboard_rowLayout(), + }, + { + ResourceName: "google_monitoring_dashboard.dashboard", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"project"}, + }, + }, + }) +} + +func TestAccMonitoringDashboard_update(t *testing.T) { + t.Parallel() + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckMonitoringDashboardDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccMonitoringDashboard_rowLayout(), + }, + { + ResourceName: "google_monitoring_dashboard.dashboard", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"project"}, + }, + { + Config: testAccMonitoringDashboard_basic(), + }, + { + ResourceName: "google_monitoring_dashboard.dashboard", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"project"}, + }, + { + Config: testAccMonitoringDashboard_gridLayout(), + }, + { + ResourceName: "google_monitoring_dashboard.dashboard", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"project"}, + }, + }, + }) +} + +func testAccCheckMonitoringDashboardDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_monitoring_dashboard" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := googleProviderConfig(t) + + url, err := replaceVarsForTest(config, rs, "{{MonitoringBasePath}}v1/{{name}}") + if err != nil { + return err + } + + _, err = sendRequest(config, "GET", "", url, nil, isMonitoringRetryableError) + if err == nil { + return fmt.Errorf("MonitoringDashboard still exists at %s", url) + } + } + + return nil + } +} + +func testAccMonitoringDashboard_basic() string { + return fmt.Sprintf(` +resource "google_monitoring_dashboard" "dashboard" { + dashboard_json = < If you're importing a resource with beta features, make sure to include `-provider=google-beta` +as an argument so that Terraform uses the correct provider to import your resource. + +## User Project Overrides + +This resource supports [User Project Overrides](https://www.terraform.io/docs/providers/google/guides/provider_reference.html#user_project_override). diff --git a/website/google.erb b/website/google.erb index 19762120822..831ef7cffac 100644 --- a/website/google.erb +++ b/website/google.erb @@ -279,6 +279,10 @@ google_monitoring_alert_policy +
  • + google_monitoring_dashboard +
  • +
  • google_monitoring_group