diff --git a/datadog/resource_datadog_monitor.go b/datadog/resource_datadog_monitor.go index a9a01d227..953392db6 100644 --- a/datadog/resource_datadog_monitor.go +++ b/datadog/resource_datadog_monitor.go @@ -8,6 +8,7 @@ import ( "sort" "strconv" "strings" + "time" "github.com/hashicorp/terraform/helper/schema" "github.com/zorkian/go-datadog-api" @@ -157,9 +158,10 @@ func resourceDatadogMonitor() *schema.Resource { Optional: true, }, "silenced": { - Type: schema.TypeMap, - Optional: true, - Elem: schema.TypeInt, + Type: schema.TypeMap, + Optional: true, + Elem: schema.TypeInt, + Deprecated: "use Downtime Resource instead", }, "include_tags": { Type: schema.TypeBool, @@ -323,6 +325,19 @@ func resourceDatadogMonitorExists(d *schema.ResourceData, meta interface{}) (b b return true, nil } +func getUnmutedScopes(d *schema.ResourceData) []string { + var unmuteScopes []string + if attr, ok := d.GetOk("silenced"); ok { + for k, v := range attr.(map[string]interface{}) { + if v.(int) == -1 { + unmuteScopes = append(unmuteScopes, k) + } + } + log.Printf("[DEBUG] Unmute Scopes are: %v", unmuteScopes) + } + return unmuteScopes +} + func resourceDatadogMonitorCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*datadog.Client) @@ -398,7 +413,6 @@ func resourceDatadogMonitorRead(d *schema.ResourceData, meta interface{}) error d.Set("notify_audit", m.Options.GetNotifyAudit()) d.Set("timeout_h", m.Options.GetTimeoutH()) d.Set("escalation_message", m.Options.GetEscalationMessage()) - d.Set("silenced", m.Options.Silenced) d.Set("include_tags", m.Options.GetIncludeTags()) d.Set("tags", tags) d.Set("require_full_window", m.Options.GetRequireFullWindow()) // TODO Is this one of those options that we neeed to check? @@ -418,6 +432,26 @@ func resourceDatadogMonitorRead(d *schema.ResourceData, meta interface{}) error d.Set("query_config", queryConfig) } + // The Datadog API doesn't return old timestamps or support a special value for unmuting scopes + // So we provide this functionality by saving values to the state + apiSilenced := m.Options.Silenced + configSilenced := d.Get("silenced").(map[string]interface{}) + + for _, scope := range getUnmutedScopes(d) { + if _, ok := apiSilenced[scope]; !ok { + apiSilenced[scope] = -1 + } + } + + // Ignore any timestamps in the past that aren't -1 or 0 + for k, v := range configSilenced { + if v.(int) < int(time.Now().Unix()) && v.(int) != 0 && v.(int) != -1 { + // sync the state with whats in the config so its ignored + apiSilenced[k] = v.(int) + } + } + d.Set("silenced", apiSilenced) + return nil } @@ -511,12 +545,15 @@ func resourceDatadogMonitorUpdate(d *schema.ResourceData, meta interface{}) erro if attr, ok := d.GetOk("escalation_message"); ok { o.SetEscalationMessage(attr.(string)) } + silenced := false + configuredSilenced := map[string]int{} if attr, ok := d.GetOk("silenced"); ok { // TODO: this is not very defensive, test if we can fail non int input s := make(map[string]int) for k, v := range attr.(map[string]interface{}) { s[k] = v.(int) + configuredSilenced[k] = v.(int) } o.Silenced = s silenced = true @@ -549,14 +586,31 @@ func resourceDatadogMonitorUpdate(d *schema.ResourceData, meta interface{}) erro return retval } - if _, ok := d.GetOk("silenced"); ok && !silenced { - // This means the monitor must be manually unmuted since the API - // wouldn't do it automatically when `silenced` is just missing + // if the silenced section was removed from the config, we unmute it via the API + // The API wouldn't automatically unmute the monitor if the config is just missing + // else we check what other silenced scopes were added from API response in the + // "read" above and add them to "unmutedScopes" to be explicitly unmuted (because + // they're "drift") + unmutedScopes := getUnmutedScopes(d) + if newSilenced, ok := d.GetOk("silenced"); ok && !silenced { retval = client.UnmuteMonitorScopes(*m.Id, &datadog.UnmuteMonitorScopes{AllScopes: datadog.Bool(true)}) d.Set("silenced", map[string]int{}) + } else { + for scope := range newSilenced.(map[string]interface{}) { + if _, ok := configuredSilenced[scope]; !ok { + unmutedScopes = append(unmutedScopes, scope) + } + } } - return retval + // Similarly, if the silenced attribute is -1, lets unmute those scopes + if len(unmutedScopes) != 0 { + for _, scope := range unmutedScopes { + client.UnmuteMonitorScopes(*m.Id, &datadog.UnmuteMonitorScopes{Scope: &scope}) + } + } + + return resourceDatadogMonitorRead(d, meta) } func resourceDatadogMonitorDelete(d *schema.ResourceData, meta interface{}) error { diff --git a/datadog/resource_datadog_monitor_test.go b/datadog/resource_datadog_monitor_test.go index 0581e5e3a..be263243b 100644 --- a/datadog/resource_datadog_monitor_test.go +++ b/datadog/resource_datadog_monitor_test.go @@ -604,6 +604,154 @@ func testAccCheckDatadogMonitorExists(n string) resource.TestCheckFunc { } } +func TestAccDatadogMonitor_SilencedRemove(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogMonitorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogMonitorSilenceZero, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogMonitorExists("datadog_monitor.foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "name", "name for monitor foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "message", "some message Notify: @hipchat-channel"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "query", "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "type", "query alert"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "silenced.*", "0"), + ), + }, + { + Config: testAccCheckDatadogMonitorSilenceUnmute, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogMonitorExists("datadog_monitor.foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "name", "name for monitor foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "type", "query alert"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "silenced.*", "-1"), + ), + }, + }, + }) +} + +func TestAccDatadogMonitor_SilencedUpdateNoDiff(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogMonitorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogMonitorSilenceZero, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogMonitorExists("datadog_monitor.foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "name", "name for monitor foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "message", "some message Notify: @hipchat-channel"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "query", "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "type", "query alert"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "silenced.*", "0"), + ), + }, + { + Config: testAccCheckDatadogMonitorSilenceZero, + ExpectNonEmptyPlan: false, + }, + }, + }) +} + +func TestAccDatadogMonitor_SilencedUpdatePastTimestamp(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogMonitorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogMonitorSilenceZero, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogMonitorExists("datadog_monitor.foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "name", "name for monitor foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "message", "some message Notify: @hipchat-channel"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "query", "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "type", "query alert"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "silenced.*", "0"), + ), + }, + { + Config: testAccCheckDatadogMonitorSilencePastTimestamp, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "silenced.*", "1559759717", + ), + ), + }, + { + Config: testAccCheckDatadogMonitorSilencePastTimestamp, + ExpectNonEmptyPlan: false, + }, + }, + }) +} + +const testAccCheckDatadogMonitorSilenceZero = ` +resource "datadog_monitor" "foo" { + name = "name for monitor foo" + type = "query alert" + message = "some message Notify: @hipchat-channel" + + query = "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2" + + silenced = { + "*" = 0 + } +} +` + +const testAccCheckDatadogMonitorSilenceUnmute = ` +resource "datadog_monitor" "foo" { + name = "name for monitor foo" + type = "query alert" + message = "some message Notify: @hipchat-channel" + + query = "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2" + + silenced = { + "*" = -1 + } +} +` + +const testAccCheckDatadogMonitorSilencePastTimestamp = ` +resource "datadog_monitor" "foo" { + name = "name for monitor foo" + type = "query alert" + message = "some message Notify: @hipchat-channel" + + query = "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2" + + silenced = { + "*" = 1559759717 + } +} +` + const testAccCheckDatadogMonitorConfig = ` resource "datadog_monitor" "foo" { name = "name for monitor foo" diff --git a/website/docs/r/monitor.html.markdown b/website/docs/r/monitor.html.markdown index 8ee407dd3..f76521981 100644 --- a/website/docs/r/monitor.html.markdown +++ b/website/docs/r/monitor.html.markdown @@ -118,7 +118,8 @@ The following arguments are supported: * `threshold_windows` (Optional) A mapping containing `recovery_window` and `trigger_window` values, e.g. `last_15m`. Can only be used for anomaly monitors. * `recovery_window` describes how long an anomalous metric must be normal before the alert recovers. * `trigger_window` describes how long a metric must be anomalous before an alert triggers. -* `silenced` (Optional) Each scope will be muted until the given POSIX timestamp or forever if the value is 0. +* `silenced` (Optional) Each scope will be muted until the given POSIX timestamp or forever if the value is 0. Use `-1` if you want to unmute the scope. **Deprecated** The `silenced` parameter is being deprecated in favor of the downtime resource. This will be removed in the next major version of the Terraform Provider. + To mute the alert completely: silenced = {