provider/datadog: Various enhancements

- Don't drop wildcard if it's the only one.
- Remove monitor resource, it's been replaced by metric_alert,
  outlier_alert and service_check
- Refactor to be closer to the API; each resource creates exactly *one*
  resource, not 2, this removes much unneeded complexity. A warning
  threshold is now supported by the API.
- Remove fuzzy resources like graph, and resources that used them for
  dashboard and screenboards. I'd welcome these resources, but the
  current state of Terraform and the Datadog API does not allow these to
  be implemented in a clean way.
- Support multiple thresholds for metric alerts, remove notify argument.
This commit is contained in:
Otto Jongerius 2015-10-23 14:42:19 +11:00 committed by James Nugent
parent 1b84048aef
commit f407eea3f7
12 changed files with 1082 additions and 240 deletions

View File

@ -0,0 +1,23 @@
package datadog
import (
"log"
"github.com/zorkian/go-datadog-api"
)
// Config holds API and APP keys to authenticate to Datadog.
type Config struct {
APIKey string
APPKey string
}
// Client returns a new Datadog client.
func (c *Config) Client() (*datadog.Client, error) {
client := datadog.NewClient(c.APIKey, c.APPKey)
log.Printf("[INFO] Datadog Client configured ")
return client, nil
}

View File

@ -1,10 +1,13 @@
package datadog
import (
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
// Provider returns a terraform.ResourceProvider.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
@ -19,15 +22,25 @@ func Provider() terraform.ResourceProvider {
DefaultFunc: schema.EnvDefaultFunc("DATADOG_APP_KEY", nil),
},
},
ResourcesMap: map[string]*schema.Resource{
"datadog_monitor_metric": datadogMonitorResource(),
"datadog_service_check": resourceDatadogServiceCheck(),
"datadog_metric_alert": resourceDatadogMetricAlert(),
"datadog_outlier_alert": resourceDatadogOutlierAlert(),
},
ConfigureFunc: providerConfigure,
}
}
func providerConfigure(rd *schema.ResourceData) (interface{}, error) {
apiKey := rd.Get("api_key").(string)
appKey := rd.Get("app_key").(string)
return map[string]string{"api_key": apiKey, "app_key": appKey}, nil
// ProviderConfigure returns a configured client.
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
APIKey: d.Get("api_key").(string),
APPKey: d.Get("app_key").(string),
}
log.Println("[INFO] Initializing Datadog client")
return config.Client()
}

View File

@ -0,0 +1,38 @@
package datadog
import (
"os"
"testing"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider
func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"datadog": testAccProvider,
}
}
func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("DATADOG_API_KEY"); v == "" {
t.Fatal("DATADOG_API_KEY must be set for acceptance tests")
}
if v := os.Getenv("DATADOG_APP_KEY"); v == "" {
t.Fatal("DATADOG_APP_KEY must be set for acceptance tests")
}
}

View File

@ -0,0 +1,180 @@
package datadog
import (
"bytes"
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/zorkian/go-datadog-api"
)
// resourceDatadogMetricAlert is a Datadog monitor resource
func resourceDatadogMetricAlert() *schema.Resource {
return &schema.Resource{
Create: resourceDatadogMetricAlertCreate,
Read: resourceDatadogGenericRead,
Update: resourceDatadogMetricAlertUpdate,
Delete: resourceDatadogGenericDelete,
Exists: resourceDatadogGenericExists,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"metric": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"tags": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"keys": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"time_aggr": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"time_window": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"space_aggr": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"operator": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"message": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"thresholds": thresholdSchema(),
// Additional Settings
"notify_no_data": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"no_data_timeframe": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"renotify_interval": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 0,
},
},
}
}
// buildMonitorStruct returns a monitor struct
func buildMetricAlertStruct(d *schema.ResourceData) *datadog.Monitor {
name := d.Get("name").(string)
message := d.Get("message").(string)
timeAggr := d.Get("time_aggr").(string)
timeWindow := d.Get("time_window").(string)
spaceAggr := d.Get("space_aggr").(string)
metric := d.Get("metric").(string)
// Tags are are no separate resource/gettable, so some trickery is needed
var buffer bytes.Buffer
if raw, ok := d.GetOk("tags"); ok {
list := raw.([]interface{})
length := (len(list) - 1)
for i, v := range list {
buffer.WriteString(fmt.Sprintf("%s", v))
if i != length {
buffer.WriteString(",")
}
}
}
tagsParsed := buffer.String()
// Keys are used for multi alerts
var b bytes.Buffer
if raw, ok := d.GetOk("keys"); ok {
list := raw.([]interface{})
b.WriteString("by {")
length := (len(list) - 1)
for i, v := range list {
b.WriteString(fmt.Sprintf("%s", v))
if i != length {
b.WriteString(",")
}
}
b.WriteString("}")
}
keys := b.String()
threshold, thresholds := getThresholds(d)
operator := d.Get("operator").(string)
query := fmt.Sprintf("%s(%s):%s:%s{%s} %s %s %s", timeAggr,
timeWindow,
spaceAggr,
metric,
tagsParsed,
keys,
operator,
threshold)
log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", query))
o := datadog.Options{
NotifyNoData: d.Get("notify_no_data").(bool),
NoDataTimeframe: d.Get("no_data_timeframe").(int),
RenotifyInterval: d.Get("renotify_interval").(int),
Thresholds: thresholds,
}
m := datadog.Monitor{
Type: "metric alert",
Query: query,
Name: name,
Message: message,
Options: o,
}
return &m
}
// resourceDatadogMetricAlertCreate creates a monitor.
func resourceDatadogMetricAlertCreate(d *schema.ResourceData, meta interface{}) error {
m := buildMetricAlertStruct(d)
if err := monitorCreator(d, meta, m); err != nil {
return err
}
return nil
}
// resourceDatadogMetricAlertUpdate updates a monitor.
func resourceDatadogMetricAlertUpdate(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] running update.")
m := buildMetricAlertStruct(d)
if err := monitorUpdater(d, meta, m); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,111 @@
package datadog
import (
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/zorkian/go-datadog-api"
)
func TestAccDatadogMetricAlert_Basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDatadogMetricAlertDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckDatadogMetricAlertConfigBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDatadogMetricAlertExists("datadog_metric_alert.foo"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "name", "name for metric_alert foo"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "message", "{{#is_alert}}Metric alert foo is critical"+
"{{/is_alert}}\n{{#is_warning}}Metric alert foo is at warning "+
"level{{/is_warning}}\n{{#is_recovery}}Metric alert foo has "+
"recovered{{/is_recovery}}\nNotify: @hipchat-channel\n"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "metric", "aws.ec2.cpu"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "tags.0", "environment:foo"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "tags.1", "host:foo"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "tags.#", "2"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "keys.0", "host"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "keys.#", "1"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "time_aggr", "avg"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "time_window", "last_1h"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "space_aggr", "avg"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "operator", ">"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "notify_no_data", "false"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "renotify_interval", "60"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "thresholds.ok", "0"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "thresholds.warning", "1"),
resource.TestCheckResourceAttr(
"datadog_metric_alert.foo", "thresholds.critical", "2"),
),
},
},
})
}
func testAccCheckDatadogMetricAlertDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*datadog.Client)
if err := destroyHelper(s, client); err != nil {
return err
}
return nil
}
func testAccCheckDatadogMetricAlertExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*datadog.Client)
if err := existsHelper(s, client); err != nil {
return err
}
return nil
}
}
const testAccCheckDatadogMetricAlertConfigBasic = `
resource "datadog_metric_alert" "foo" {
name = "name for metric_alert foo"
message = <<EOF
{{#is_alert}}Metric alert foo is critical{{/is_alert}}
{{#is_warning}}Metric alert foo is at warning level{{/is_warning}}
{{#is_recovery}}Metric alert foo has recovered{{/is_recovery}}
Notify: @hipchat-channel
EOF
metric = "aws.ec2.cpu"
tags = ["environment:foo", "host:foo"]
keys = ["host"]
time_aggr = "avg" // avg, sum, max, min, change, or pct_change
time_window = "last_1h" // last_#m (5, 10, 15, 30), last_#h (1, 2, 4), or last_1d
space_aggr = "avg" // avg, sum, min, or max
operator = ">" // <, <=, >, >=, ==, or !=
thresholds {
ok = 0
warning = 1
critical = 2
}
notify_no_data = false
renotify_interval = 60
}
`

View File

@ -1,235 +0,0 @@
package datadog
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/schema"
)
const (
monitorEndpoint = "https://app.datadoghq.com/api/v1/monitor"
)
func datadogMonitorResource() *schema.Resource {
return &schema.Resource{
Create: resourceMonitorCreate,
Read: resourceMonitorRead,
Update: resourceMonitorUpdate,
Delete: resourceMonitorDelete,
Exists: resourceMonitorExists,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
// Metric and Monitor settings
"metric": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"metric_tags": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "*",
},
"time_aggr": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"time_window": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"space_aggr": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"operator": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"message": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
// Alert Settings
"warning": &schema.Schema{
Type: schema.TypeMap,
Required: true,
},
"critical": &schema.Schema{
Type: schema.TypeMap,
Required: true,
},
// Additional Settings
"notify_no_data": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"no_data_timeframe": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
},
}
}
func getIDFromResponse(h *http.Response) (string, error) {
body, err := ioutil.ReadAll(h.Body)
if err != nil {
return "", err
}
h.Body.Close()
log.Println(h)
log.Println(string(body))
v := map[string]interface{}{}
err = json.Unmarshal(body, &v)
if err != nil {
return "", err
}
if id, ok := v["id"]; ok {
return strconv.Itoa(int(id.(float64))), nil
}
return "", fmt.Errorf("error getting ID from response %s", h.Status)
}
func marshalMetric(d *schema.ResourceData, typeStr string) ([]byte, error) {
name := d.Get("name").(string)
message := d.Get("message").(string)
timeAggr := d.Get("time_aggr").(string)
timeWindow := d.Get("time_window").(string)
spaceAggr := d.Get("space_aggr").(string)
metric := d.Get("metric").(string)
tags := d.Get("metric_tags").(string)
operator := d.Get("operator").(string)
query := fmt.Sprintf("%s(%s):%s:%s{%s} %s %s", timeAggr, timeWindow, spaceAggr, metric, tags, operator, d.Get(fmt.Sprintf("%s.threshold", typeStr)))
log.Println(query)
m := map[string]interface{}{
"type": "metric alert",
"query": query,
"name": fmt.Sprintf("[%s] %s", typeStr, name),
"message": fmt.Sprintf("%s %s", message, d.Get(fmt.Sprintf("%s.notify", typeStr))),
"options": map[string]interface{}{
"notify_no_data": d.Get("notify_no_data").(bool),
"no_data_timeframe": d.Get("no_data_timeframe").(int),
},
}
return json.Marshal(m)
}
func authSuffix(meta interface{}) string {
m := meta.(map[string]string)
return fmt.Sprintf("?api_key=%s&application_key=%s", m["api_key"], m["app_key"])
}
func resourceMonitorCreate(d *schema.ResourceData, meta interface{}) error {
warningBody, err := marshalMetric(d, "warning")
if err != nil {
return err
}
criticalBody, err := marshalMetric(d, "critical")
if err != nil {
return err
}
resW, err := http.Post(fmt.Sprintf("%s%s", monitorEndpoint, authSuffix(meta)), "application/json", bytes.NewReader(warningBody))
if err != nil {
return fmt.Errorf("error creating warning: %s", err.Error())
}
resC, err := http.Post(fmt.Sprintf("%s%s", monitorEndpoint, authSuffix(meta)), "application/json", bytes.NewReader(criticalBody))
if err != nil {
return fmt.Errorf("error creating critical: %s", err.Error())
}
warningMonitorID, err := getIDFromResponse(resW)
if err != nil {
return err
}
criticalMonitorID, err := getIDFromResponse(resC)
if err != nil {
return err
}
d.SetId(fmt.Sprintf("%s__%s", warningMonitorID, criticalMonitorID))
return nil
}
func resourceMonitorDelete(d *schema.ResourceData, meta interface{}) (e error) {
for _, v := range strings.Split(d.Id(), "__") {
client := http.Client{}
req, _ := http.NewRequest("DELETE", fmt.Sprintf("%s/%s%s", monitorEndpoint, v, authSuffix(meta)), nil)
_, err := client.Do(req)
e = err
}
return
}
func resourceMonitorExists(d *schema.ResourceData, meta interface{}) (b bool, e error) {
b = true
for _, v := range strings.Split(d.Id(), "__") {
res, err := http.Get(fmt.Sprintf("%s/%s%s", monitorEndpoint, v, authSuffix(meta)))
if err != nil {
e = err
continue
}
if res.StatusCode > 400 {
b = false
continue
}
b = b && true
}
if !b {
e = resourceMonitorDelete(d, meta)
}
return
}
func resourceMonitorRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func resourceMonitorUpdate(d *schema.ResourceData, meta interface{}) error {
split := strings.Split(d.Id(), "__")
warningID, criticalID := split[0], split[1]
warningBody, _ := marshalMetric(d, "warning")
criticalBody, _ := marshalMetric(d, "critical")
client := http.Client{}
reqW, _ := http.NewRequest("PUT", fmt.Sprintf("%s/%s%s", monitorEndpoint, warningID, authSuffix(meta)), bytes.NewReader(warningBody))
resW, err := client.Do(reqW)
if err != nil {
return fmt.Errorf("error updating warning: %s", err.Error())
}
resW.Body.Close()
if resW.StatusCode > 400 {
return fmt.Errorf("error updating warning monitor: %s", resW.Status)
}
reqC, _ := http.NewRequest("PUT", fmt.Sprintf("%s/%s%s", monitorEndpoint, criticalID, authSuffix(meta)), bytes.NewReader(criticalBody))
resC, err := client.Do(reqC)
if err != nil {
return fmt.Errorf("error updating critical: %s", err.Error())
}
resW.Body.Close()
if resW.StatusCode > 400 {
return fmt.Errorf("error updating critical monitor: %s", resC.Status)
}
return nil
}

View File

@ -0,0 +1,184 @@
package datadog
import (
"bytes"
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/zorkian/go-datadog-api"
)
// resourceDatadogOutlierAlert is a Datadog monitor resource
func resourceDatadogOutlierAlert() *schema.Resource {
return &schema.Resource{
Create: resourceDatadogOutlierAlertCreate,
Read: resourceDatadogGenericRead,
Update: resourceDatadogOutlierAlertUpdate,
Delete: resourceDatadogGenericDelete,
Exists: resourceDatadogGenericExists,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"metric": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"tags": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"keys": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"time_aggr": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"time_window": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"space_aggr": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"message": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"threshold": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
// Additional Settings
"notify_no_data": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"no_data_timeframe": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"algorithm": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "dbscan",
},
"renotify_interval": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 0,
},
},
}
}
// buildMonitorStruct returns a monitor struct
func buildOutlierAlertStruct(d *schema.ResourceData) *datadog.Monitor {
name := d.Get("name").(string)
message := d.Get("message").(string)
timeAggr := d.Get("time_aggr").(string)
timeWindow := d.Get("time_window").(string)
spaceAggr := d.Get("space_aggr").(string)
metric := d.Get("metric").(string)
algorithm := d.Get("algorithm").(string)
// Tags are are no separate resource/gettable, so some trickery is needed
var buffer bytes.Buffer
if raw, ok := d.GetOk("tags"); ok {
list := raw.([]interface{})
length := (len(list) - 1)
for i, v := range list {
if length > 1 && v == "*" {
log.Print(fmt.Sprintf("[DEBUG] found wildcard, this is not supported for this type: %s", v))
continue
}
buffer.WriteString(fmt.Sprintf("%s", v))
if i != length {
buffer.WriteString(",")
}
}
}
tagsParsed := buffer.String()
// Keys are used for multi alerts
var b bytes.Buffer
if raw, ok := d.GetOk("keys"); ok {
list := raw.([]interface{})
b.WriteString("by {")
length := (len(list) - 1)
for i, v := range list {
b.WriteString(fmt.Sprintf("%s", v))
if i != length {
b.WriteString(",")
}
}
b.WriteString("}")
}
keys := b.String()
query := fmt.Sprintf("%s(%s):outliers(%s:%s{%s} %s, '%s',%s) > 0", timeAggr,
timeWindow,
spaceAggr,
metric,
tagsParsed,
keys,
algorithm,
d.Get("threshold"))
log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", query))
o := datadog.Options{
NotifyNoData: d.Get("notify_no_data").(bool),
NoDataTimeframe: d.Get("no_data_timeframe").(int),
RenotifyInterval: d.Get("renotify_interval").(int),
}
m := datadog.Monitor{
Type: "query alert",
Query: query,
Name: name,
Message: message,
Options: o,
}
return &m
}
// resourceDatadogOutlierAlertCreate creates a monitor.
func resourceDatadogOutlierAlertCreate(d *schema.ResourceData, meta interface{}) error {
m := buildOutlierAlertStruct(d)
if err := monitorCreator(d, meta, m); err != nil {
return err
}
return nil
}
// resourceDatadogOutlierAlertUpdate updates a monitor.
func resourceDatadogOutlierAlertUpdate(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] running update.")
m := buildOutlierAlertStruct(d)
if err := monitorUpdater(d, meta, m); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,97 @@
package datadog
import (
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/zorkian/go-datadog-api"
)
func TestAccDatadogOutlierAlert_Basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDatadogOutlierAlertDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckDatadogOutlierAlertConfigBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDatadogOutlierAlertExists("datadog_outlier_alert.foo"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "name", "name for outlier_alert foo"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "message", "description for outlier_alert foo @hipchat-name"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "metric", "system.load.5"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "tags.0", "environment:foo"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "tags.1", "host:foo"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "tags.#", "2"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "keys.0", "host"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "keys.#", "1"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "time_aggr", "avg"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "time_window", "last_1h"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "space_aggr", "avg"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "notify_no_data", "false"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "algorithm", "mad"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "renotify_interval", "60"),
resource.TestCheckResourceAttr(
"datadog_outlier_alert.foo", "threshold", "2"),
),
},
},
})
}
func testAccCheckDatadogOutlierAlertDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*datadog.Client)
if err := destroyHelper(s, client); err != nil {
return err
}
return nil
}
func testAccCheckDatadogOutlierAlertExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*datadog.Client)
if err := existsHelper(s, client); err != nil {
return err
}
return nil
}
}
const testAccCheckDatadogOutlierAlertConfigBasic = `
resource "datadog_outlier_alert" "foo" {
name = "name for outlier_alert foo"
message = "description for outlier_alert foo @hipchat-name"
algorithm = "mad"
metric = "system.load.5"
tags = ["environment:foo", "host:foo"]
keys = ["host"]
time_aggr = "avg" // avg, sum, max, min, change, or pct_change
time_window = "last_1h" // last_#m (5, 10, 15, 30), last_#h (1, 2, 4), or last_1d
space_aggr = "avg" // avg, sum, min, or max
threshold = 2.0
notify_no_data = false
renotify_interval = 60
}
`

View File

@ -0,0 +1,163 @@
package datadog
import (
"bytes"
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/zorkian/go-datadog-api"
)
// resourceDatadogServiceCheck is a Datadog monitor resource
func resourceDatadogServiceCheck() *schema.Resource {
return &schema.Resource{
Create: resourceDatadogServiceCheckCreate,
Read: resourceDatadogGenericRead,
Update: resourceDatadogServiceCheckUpdate,
Delete: resourceDatadogGenericDelete,
Exists: resourceDatadogGenericExists,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"check": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"thresholds": thresholdSchema(),
"tags": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"keys": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"message": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
// Additional Settings
"notify_no_data": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"no_data_timeframe": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"renotify_interval": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 0,
},
},
}
}
// buildServiceCheckStruct returns a monitor struct
func buildServiceCheckStruct(d *schema.ResourceData) *datadog.Monitor {
log.Print("[DEBUG] building monitor struct")
name := d.Get("name").(string)
message := d.Get("message").(string)
// Tags are are no separate resource/gettable, so some trickery is needed
var buffer bytes.Buffer
if raw, ok := d.GetOk("tags"); ok {
list := raw.([]interface{})
length := (len(list) - 1)
for i, v := range list {
buffer.WriteString(fmt.Sprintf("\"%s\"", v))
if i != length {
buffer.WriteString(",")
}
}
}
tagsParsed := buffer.String()
// Keys are used for multi alerts
var b bytes.Buffer
if raw, ok := d.GetOk("keys"); ok {
list := raw.([]interface{})
b.WriteString(".by(")
length := (len(list) - 1)
for i, v := range list {
b.WriteString(fmt.Sprintf("\"%s\"", v))
if i != length {
b.WriteString(",")
}
}
b.WriteString(")")
}
keys := b.String()
var monitorName string
var query string
check := d.Get("check").(string)
// Examples queries
// "http.can_connect".over("instance:buildeng_http","production").last(2).count_by_status()
// "http.can_connect".over("*").by("host","instance","url").last(2).count_by_status()
checkCount, thresholds := getThresholds(d)
query = fmt.Sprintf("\"%s\".over(%s)%s.last(%s).count_by_status()", check, tagsParsed, keys, checkCount)
log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", query))
monitorName = name
o := datadog.Options{
NotifyNoData: d.Get("notify_no_data").(bool),
NoDataTimeframe: d.Get("no_data_timeframe").(int),
RenotifyInterval: d.Get("renotify_interval").(int),
Thresholds: thresholds,
}
m := datadog.Monitor{
Type: "service check",
Query: query,
Name: monitorName,
Message: message,
Options: o,
}
return &m
}
// resourceDatadogServiceCheckCreate creates a monitor.
func resourceDatadogServiceCheckCreate(d *schema.ResourceData, meta interface{}) error {
log.Print("[DEBUG] creating monitor")
m := buildServiceCheckStruct(d)
if err := monitorCreator(d, meta, m); err != nil {
return err
}
return nil
}
// resourceDatadogServiceCheckUpdate updates a monitor.
func resourceDatadogServiceCheckUpdate(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] running update.")
m := buildServiceCheckStruct(d)
if err := monitorUpdater(d, meta, m); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,96 @@
package datadog
import (
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/zorkian/go-datadog-api"
)
func TestAccDatadogServiceCheck_Basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDatadogServiceCheckDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckDatadogServiceCheckConfigBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDatadogServiceCheckExists("datadog_service_check.bar"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "name", "name for service check bar"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "message", "{{#is_alert}}Service check bar is critical"+
"{{/is_alert}}\n{{#is_warning}}Service check bar is at warning "+
"level{{/is_warning}}\n{{#is_recovery}}Service check bar has "+
"recovered{{/is_recovery}}\nNotify: @hipchat-channel\n"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "check", "datadog.agent.up"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "notify_no_data", "false"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "tags.0", "environment:foo"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "tags.1", "host:bar"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "tags.#", "2"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "keys.0", "foo"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "keys.1", "bar"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "keys.#", "2"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "thresholds.ok", "0"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "thresholds.warning", "1"),
resource.TestCheckResourceAttr(
"datadog_service_check.bar", "thresholds.critical", "2"),
),
},
},
})
}
func testAccCheckDatadogServiceCheckDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*datadog.Client)
if err := destroyHelper(s, client); err != nil {
return err
}
return nil
}
func testAccCheckDatadogServiceCheckExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*datadog.Client)
if err := existsHelper(s, client); err != nil {
return err
}
return nil
}
}
const testAccCheckDatadogServiceCheckConfigBasic = `
resource "datadog_service_check" "bar" {
name = "name for service check bar"
message = <<EOF
{{#is_alert}}Service check bar is critical{{/is_alert}}
{{#is_warning}}Service check bar is at warning level{{/is_warning}}
{{#is_recovery}}Service check bar has recovered{{/is_recovery}}
Notify: @hipchat-channel
EOF
tags = ["environment:foo", "host:bar"]
keys = ["foo", "bar"]
check = "datadog.agent.up"
thresholds {
ok = 0
warning = 1
critical = 2
}
notify_no_data = false
}
`

View File

@ -0,0 +1,138 @@
package datadog
import (
"encoding/json"
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"github.com/zorkian/go-datadog-api"
"strconv"
"strings"
)
func thresholdSchema() *schema.Schema {
return &schema.Schema{
Type: schema.TypeMap,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"ok": &schema.Schema{
Type: schema.TypeFloat,
Optional: true,
},
"warning": &schema.Schema{
Type: schema.TypeFloat,
Optional: true,
},
"critical": &schema.Schema{
Type: schema.TypeFloat,
Required: true,
},
},
},
}
}
func getThresholds(d *schema.ResourceData) (string, datadog.ThresholdCount) {
t := datadog.ThresholdCount{}
var threshold string
if r, ok := d.GetOk("thresholds.ok"); ok {
t.Ok = json.Number(r.(string))
}
if r, ok := d.GetOk("thresholds.warning"); ok {
t.Warning = json.Number(r.(string))
}
if r, ok := d.GetOk("thresholds.critical"); ok {
threshold = r.(string)
t.Critical = json.Number(r.(string))
}
return threshold, t
}
func resourceDatadogGenericDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*datadog.Client)
i, err := strconv.Atoi(d.Id())
if err != nil {
return err
}
if err = client.DeleteMonitor(i); err != nil {
return err
}
return nil
}
func resourceDatadogGenericExists(d *schema.ResourceData, meta interface{}) (b bool, e error) {
// Exists - This is called to verify a resource still exists. It is called prior to Read,
// and lowers the burden of Read to be able to assume the resource exists.
client := meta.(*datadog.Client)
// Workaround to handle upgrades from < 0.0.4
if strings.Contains(d.Id(), "__") {
return false, fmt.Errorf("Monitor ID contains __, which is pre v0.0.4 old behaviour.\n You have the following options:\n" +
" * Run https://github.com/ojongerius/terraform-provider-datadog/blob/master/scripts/migration_helper.py to generate a new statefile and clean up monitors\n" +
" * Mannualy fix this by deleting all your metric_check resources and recreate them, " +
"or manually remove half of the resources and hack the state file.\n")
}
i, err := strconv.Atoi(d.Id())
if err != nil {
return false, err
}
if _, err = client.GetMonitor(i); err != nil {
if strings.Contains(err.Error(), "404 Not Found") {
return false, nil
}
return false, err
}
return true, nil
}
func resourceDatadogGenericRead(d *schema.ResourceData, meta interface{}) error {
// TODO: Add support for read function.
/* Read - This is called to resync the local state with the remote state.
Terraform guarantees that an existing ID will be set. This ID should be
used to look up the resource. Any remote data should be updated into the
local data. No changes to the remote resource are to be made.
*/
return nil
}
func monitorCreator(d *schema.ResourceData, meta interface{}, m *datadog.Monitor) error {
client := meta.(*datadog.Client)
m, err := client.CreateMonitor(m)
if err != nil {
return fmt.Errorf("error updating montor: %s", err.Error())
}
d.SetId(strconv.Itoa(m.Id))
return nil
}
func monitorUpdater(d *schema.ResourceData, meta interface{}, m *datadog.Monitor) error {
client := meta.(*datadog.Client)
i, err := strconv.Atoi(d.Id())
if err != nil {
return err
}
m.Id = i
if err = client.UpdateMonitor(m); err != nil {
return fmt.Errorf("error updating montor: %s", err.Error())
}
return nil
}

View File

@ -0,0 +1,34 @@
package datadog
import (
"fmt"
"strconv"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/zorkian/go-datadog-api"
)
func destroyHelper(s *terraform.State, client *datadog.Client) error {
for _, r := range s.RootModule().Resources {
i, _ := strconv.Atoi(r.Primary.ID)
if _, err := client.GetMonitor(i); err != nil {
if strings.Contains(err.Error(), "404 Not Found") {
continue
}
return fmt.Errorf("Received an error retrieving monitor %s", err)
}
return fmt.Errorf("Monitor still exists")
}
return nil
}
func existsHelper(s *terraform.State, client *datadog.Client) error {
for _, r := range s.RootModule().Resources {
i, _ := strconv.Atoi(r.Primary.ID)
if _, err := client.GetMonitor(i); err != nil {
return fmt.Errorf("Received an error retrieving monitor %s", err)
}
}
return nil
}