diff --git a/products/monitoring/api.yaml b/products/monitoring/api.yaml index c10106bd22eb..4e0a2dbabc79 100644 --- a/products/monitoring/api.yaml +++ b/products/monitoring/api.yaml @@ -16,7 +16,7 @@ display_name: Cloud (Stackdriver) Monitoring versions: - !ruby/object:Api::Product::Version name: ga - base_url: https://monitoring.googleapis.com/v3/ + base_url: https://monitoring.googleapis.com/ scopes: - https://www.googleapis.com/auth/cloud-platform apis_required: @@ -26,8 +26,8 @@ apis_required: objects: - !ruby/object:Api::Resource name: 'AlertPolicy' - base_url: projects/{{project}}/alertPolicies - self_link: "{{name}}" + base_url: v3/projects/{{project}}/alertPolicies + self_link: "v3/{{name}}" update_verb: :PATCH update_mask: true description: | @@ -741,8 +741,8 @@ objects: - !ruby/object:Api::Resource name: 'Group' - base_url: projects/{{project}}/groups - self_link: "{{name}}" + base_url: v3/projects/{{project}}/groups + self_link: "v3/{{name}}" update_verb: :PUT description: | The description of a dynamic collection of monitored resources. Each group @@ -789,8 +789,8 @@ objects: - !ruby/object:Api::Resource name: NotificationChannel - base_url: projects/{{project}}/notificationChannels - self_link: "{{name}}" + base_url: v3/projects/{{project}}/notificationChannels + self_link: "v3/{{name}}" update_verb: :PATCH description: | A NotificationChannel is a medium through which an alert is delivered @@ -916,9 +916,9 @@ objects: - !ruby/object:Api::Resource name: Service - base_url: projects/{{project}}/services - create_url: projects/{{project}}/services?serviceId={{service_id}} - self_link: "{{name}}" + base_url: v3/projects/{{project}}/services + create_url: v3/projects/{{project}}/services?serviceId={{service_id}} + self_link: "v3/{{name}}" update_verb: :PATCH update_mask: true description: | @@ -963,10 +963,10 @@ objects: - !ruby/object:Api::Resource name: Slo - base_url: projects/{{project}}/services/{{service}}/serviceLevelObjectives + base_url: v3/projects/{{project}}/services/{{service}}/serviceLevelObjectives # name = projects/{{project}}/services/{{service}}/serviceLevelObjectives/{{slo_id}} - self_link: "{{name}}" - create_url: projects/{{project}}/services/{{service}}/serviceLevelObjectives?serviceLevelObjectiveId={{slo_id}} + self_link: "v3/{{name}}" + create_url: v3/projects/{{project}}/services/{{service}}/serviceLevelObjectives?serviceLevelObjectiveId={{slo_id}} update_verb: :PATCH update_mask: true description: | @@ -1104,8 +1104,8 @@ objects: 'Official Documentation': 'https://cloud.google.com/monitoring/uptime-checks/' api: 'https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.uptimeCheckConfigs' - base_url: projects/{{project}}/uptimeCheckConfigs - self_link: "{{name}}" + base_url: v3/projects/{{project}}/uptimeCheckConfigs + self_link: "v3/{{name}}" description: This message configures which resources and services to monitor for availability. properties: diff --git a/products/monitoring/inspec.yaml b/products/monitoring/inspec.yaml index 6975aab558e5..f86b8e91af20 100644 --- a/products/monitoring/inspec.yaml +++ b/products/monitoring/inspec.yaml @@ -18,7 +18,7 @@ overrides: !ruby/object:Overrides::ResourceOverrides additional_functions: third_party/inspec/custom_functions/alert_policy.erb singular_extra_examples: third_party/inspec/documentation/google_project_alert_policy.md plural_extra_examples: third_party/inspec/documentation/google_project_alert_policies.md - self_link: projects/{{project}}/alertPolicies/{{name}} + self_link: v3/projects/{{project}}/alertPolicies/{{name}} properties: name: !ruby/object:Overrides::Inspec::PropertyOverride override_name: policy_names diff --git a/templates/terraform/objectlib/base.go.erb b/templates/terraform/objectlib/base.go.erb index 917fecc91388..3267d0e8ca14 100644 --- a/templates/terraform/objectlib/base.go.erb +++ b/templates/terraform/objectlib/base.go.erb @@ -5,9 +5,11 @@ package google <% resource_name = product_ns + object.name properties = object.all_user_properties - api_version = @base_url.split("/")[-1] # See discussion on asset name here: https://github.com/GoogleCloudPlatform/magic-modules/pull/1520 asset_name_template = '//' + product_ns.downcase + '.googleapis.com/' + (!object.self_link.nil? && !object.self_link.empty? ? object.self_link : object.base_url + '/{{name}}') + version_regex = /\/(v\d[^\/]*)\// + api_version = version_regex.match?(asset_name_template) ? version_regex.match(asset_name_template)[1] : @base_url.split("/")[-1] + asset_name_template.gsub!(version_regex, '/') %> <%= lines(compile(object.custom_code.constants)) if object.custom_code.constants -%> diff --git a/third_party/terraform/resources/resource_monitoring_dashboard.go b/third_party/terraform/resources/resource_monitoring_dashboard.go new file mode 100644 index 000000000000..a32422f95251 --- /dev/null +++ b/third_party/terraform/resources/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/third_party/terraform/tests/resource_monitoring_dashboard_test.go b/third_party/terraform/tests/resource_monitoring_dashboard_test.go new file mode 100644 index 000000000000..55cc74ff84fb --- /dev/null +++ b/third_party/terraform/tests/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).