From 209e7ed866a47dcc98db25c81f8c9f87401be059 Mon Sep 17 00:00:00 2001 From: Louis McCormack Date: Mon, 24 Jun 2019 10:33:08 +0100 Subject: [PATCH 1/4] first stab --- README.md | 5 --- wavefront/resource_alert.go | 90 ++++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0f42f01..8202688 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,6 @@ Once you have the plugin you should remove the `_os_arch` from the end of the fi Valid provider filenames are `terraform-provider-NAME_X.X.X` or `terraform-provider-NAME_vX.X.X` -## Known Issues - -To ensure that applies of large batches of Alerts are successful you can use the `-parallelism` flag to prevent parallel resource creations -`terraform apply -parallelism=1` - ## Building and Testing ### Build the plugin. diff --git a/wavefront/resource_alert.go b/wavefront/resource_alert.go index d28c6d4..caa6276 100644 --- a/wavefront/resource_alert.go +++ b/wavefront/resource_alert.go @@ -23,15 +23,28 @@ func resourceAlert() *schema.Resource { Type: schema.TypeString, Required: true, }, + "alert_type": { + Type: schema.TypeString, + Optional: true, + Default: wavefront.AlertTypeClassic, + }, "target": { Type: schema.TypeString, Required: true, }, "condition": { Type: schema.TypeString, - Required: true, + Optional: true, StateFunc: trimSpaces, }, + "threshold_conditions": { + Type: schema.TypeMap, + Optional: true, + }, + "threshold_targets": { + Type: schema.TypeMap, + Optional: true, + }, "additional_information": { Type: schema.TypeString, Optional: true, @@ -56,7 +69,7 @@ func resourceAlert() *schema.Resource { }, "severity": { Type: schema.TypeString, - Required: true, + Optional: true, }, "tags": { Type: schema.TypeSet, @@ -71,6 +84,46 @@ func trimSpaces(d interface{}) string { return strings.TrimSpace(d.(string)) } +func mapToThresholdConditions(m map[string]string) (*wavefront.ThresholdConditions, error) { + var t wavefront.ThresholdConditions + for sev, ts := range m { + switch sev { + case "severe": + t.Severe = trimSpaces(ts) + case "warn": + t.Warn = trimSpaces(ts) + case "info": + t.Info = trimSpaces(ts) + case "smoke": + t.Smoke = trimSpaces(ts) + default: + return nil, fmt.Errorf("invalid severity: %s", sev) + } + } + + return &t, nil +} + +func mapToThresholdTargets(m map[string]string) (*wavefront.ThresholdTargets, error) { + var t wavefront.ThresholdTargets + for sev, ts := range m { + switch sev { + case "severe": + t.Severe = trimSpaces(ts) + case "warn": + t.Warn = trimSpaces(ts) + case "info": + t.Info = trimSpaces(ts) + case "smoke": + t.Smoke = trimSpaces(ts) + default: + return nil, fmt.Errorf("invalid severity: %s", sev) + } + } + + return &t, nil +} + func resourceAlertCreate(d *schema.ResourceData, m interface{}) error { alerts := m.(*wavefrontClient).client.Alerts() @@ -79,19 +132,45 @@ func resourceAlertCreate(d *schema.ResourceData, m interface{}) error { tags = append(tags, tag.(string)) } + // Do some validation here? That we either have a condition or threshold_condition + // Also need to set the type accordinglt + a := &wavefront.Alert{ Name: d.Get("name").(string), - Target: d.Get("target").(string), - Condition: trimSpaces(d.Get("condition").(string)), AdditionalInfo: trimSpaces(d.Get("additional_information").(string)), DisplayExpression: trimSpaces(d.Get("display_expression").(string)), Minutes: d.Get("minutes").(int), ResolveAfterMinutes: d.Get("resolve_after_minutes").(int), NotificationResendFrequencyMinutes: d.Get("notification_resend_frequency_minutes").(int), - Severity: d.Get("severity").(string), Tags: tags, } + if d.Get("alert_type") == wavefront.AlertTypeThreshold { //DO THIS!! YOUVE CHANGED IT + a.Type = wavefront.AlertTypeThreshold + conditions, err := mapToThresholdConditions(d.Get("threshold_conditions").(map[string]string)) + if err != nil { + return err + } + a.ThresholdConditions = *conditions + + targets, err := mapToThresholdTargets(d.Get("threshold_conditions").(map[string]string)) + if err != nil { + return err + } + a.ThresholdTargets = *targets + } else if d.Get("condition") != nil { + // This is a CLASSIC alert + a.Type = wavefront.AlertTypeClassic + a.Condition = trimSpaces(d.Get("condition").(string)) + if d.Get("severity") == nil { + return fmt.Errorf("severity must be supplied with a classic alert") + } + a.Severity = d.Get("severity").(string) + a.Target = d.Get("target").(string) + } else { + return fmt.Errorf("either condition or threshold_conditions is required") + } + // Create the alert on Wavefront err := alerts.Create(a) if err != nil { @@ -129,6 +208,7 @@ func resourceAlertRead(d *schema.ResourceData, m interface{}) error { d.Set("notification_resend_frequency_minutes", tmpAlert.NotificationResendFrequencyMinutes) d.Set("severity", tmpAlert.Severity) d.Set("tags", tmpAlert.Tags) + d.Set("threshold_conditions", tmpAlert.ThresholdConditions) return nil } From 61f6b8612349638899f0886fc79f4cbeb5053124 Mon Sep 17 00:00:00 2001 From: Louis McCormack Date: Tue, 2 Jul 2019 16:35:06 +0100 Subject: [PATCH 2/4] add support for threshold alerts --- go.mod | 2 +- go.sum | 6 + .../spaceapegames/go-wavefront/alert.go | 34 ++- .../spaceapegames/go-wavefront/event.go | 2 +- .../spaceapegames/go-wavefront/target.go | 2 +- .../spaceapegames/go-wavefront/version | 2 +- vendor/modules.txt | 2 +- wavefront/resource_alert.go | 131 +++++------- wavefront/resource_alert_test.go | 198 ++++++++++++++++++ 9 files changed, 296 insertions(+), 83 deletions(-) diff --git a/go.mod b/go.mod index 50a19b3..c48bb04 100644 --- a/go.mod +++ b/go.mod @@ -3,5 +3,5 @@ module github.com/spaceapegames/terraform-provider-wavefront require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/terraform v0.12.0 - github.com/spaceapegames/go-wavefront v0.0.0-20190424105721-72e8eb185145 + github.com/spaceapegames/go-wavefront v1.6.2 ) diff --git a/go.sum b/go.sum index 5f44b52..8efcdbe 100644 --- a/go.sum +++ b/go.sum @@ -325,6 +325,12 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaceapegames/go-wavefront v0.0.0-20190424105721-72e8eb185145 h1:iN5Rc2gJLJFZ3Io4n5qxwe6uVKyHRfTGWc0yTauAH60= github.com/spaceapegames/go-wavefront v0.0.0-20190424105721-72e8eb185145/go.mod h1:Q9wbY/SM99cOM4NvoJ2SN1bzcJUJOEzta5C7dhnT5IM= +github.com/spaceapegames/go-wavefront v0.0.0-20190627103934-9f0b48af3f8f h1:EG1F9wt5cMZ9az8TDMlMRD08HP8zdjn8f3fmsVBBhf4= +github.com/spaceapegames/go-wavefront v0.0.0-20190627103934-9f0b48af3f8f/go.mod h1:Q9wbY/SM99cOM4NvoJ2SN1bzcJUJOEzta5C7dhnT5IM= +github.com/spaceapegames/go-wavefront v1.6.1 h1:5ZezLy0NiiOFDhrgJA01c5/vZZbFcj2IL2ymdRXJzT8= +github.com/spaceapegames/go-wavefront v1.6.1/go.mod h1:Q9wbY/SM99cOM4NvoJ2SN1bzcJUJOEzta5C7dhnT5IM= +github.com/spaceapegames/go-wavefront v1.6.2 h1:5AHElze/Y6oi4DdpkYfksS4F0ItsBpV7WwuB3r159cY= +github.com/spaceapegames/go-wavefront v1.6.2/go.mod h1:Q9wbY/SM99cOM4NvoJ2SN1bzcJUJOEzta5C7dhnT5IM= github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= diff --git a/vendor/github.com/spaceapegames/go-wavefront/alert.go b/vendor/github.com/spaceapegames/go-wavefront/alert.go index 72604ad..26be4ca 100644 --- a/vendor/github.com/spaceapegames/go-wavefront/alert.go +++ b/vendor/github.com/spaceapegames/go-wavefront/alert.go @@ -6,6 +6,11 @@ import ( "io/ioutil" ) +const ( + AlertTypeThreshold = "THRESHOLD" + AlertTypeClassic = "CLASSIC" +) + // Alert represents a single Wavefront Alert type Alert struct { // Name is the name given to an Alert @@ -14,15 +19,26 @@ type Alert struct { // ID is the Wavefront-assigned ID of an existing Alert ID *string `json:"id,omitempty"` + // AlertType should be either CLASSIC or THRESHOLD + AlertType string `json:"alertType,omitempty"` + // AdditionalInfo is any extra information about the Alert AdditionalInfo string `json:"additionalInformation"` // Target is a comma-separated list of targets for the Alert - Target string `json:"target"` + Target string `json:"target,omitempty"` + + // For THRESHOLD alerts. Targets is a map[string]string. This maps severity to lists of targets. + // Valid keys are: severe, smoke, warn or info + Targets map[string]string `json:"targets"` // Condition is the condition under which the Alert will fire Condition string `json:"condition"` + // For THRESHOLD alerts. Conditions is a map[string]string. This maps severity to respective conditions. + // Valid keys are: severe, smoke, warn or info + Conditions map[string]string `json:"conditions"` + // DisplayExpression is the ts query to generate a graph of this Alert, in the UI DisplayExpression string `json:"displayExpression,omitempty"` @@ -33,19 +49,31 @@ type Alert struct { // ResolveAfterMinutes is the number of minutes the Condition must be un-met // before the Alert is considered resolved ResolveAfterMinutes int `json:"resolveAfterMinutes,omitempty"` - + // Minutes to wait before re-sending notification of firing alert. NotificationResendFrequencyMinutes int `json:"notificationResendFrequencyMinutes"` // Severity is the severity of the Alert, and can be one of SEVERE, // SMOKE, WARN or INFO - Severity string `json:"severity"` + Severity string `json:"severity,omitempty"` + + // For THRESHOLD alerts. SeverityList is a list of strings. Different severities applicable to this alert. + // Valid elements are: SEVERE, SMOKE, WARN or INFO + SeverityList []string `json:"severityList"` // Status is the current status of the Alert Status []string `json:"status"` // Tags are the tags applied to the Alert Tags []string + + FailingHostLabelPairs []SourceLabelPair `json:"failingHostLabelPairs,omitempty"` + InMaintenanceHostLabelPairs []SourceLabelPair `json:"inMaintenanceHostLabelPairs,omitempty"` +} + +type SourceLabelPair struct { + Host string `json:"host"` + Firing int `json:"firing"` } // Alerts is used to perform alert-related operations against the Wavefront API diff --git a/vendor/github.com/spaceapegames/go-wavefront/event.go b/vendor/github.com/spaceapegames/go-wavefront/event.go index 3c2d707..2690981 100644 --- a/vendor/github.com/spaceapegames/go-wavefront/event.go +++ b/vendor/github.com/spaceapegames/go-wavefront/event.go @@ -114,7 +114,7 @@ func (e Events) Find(filter []*SearchCondition, timeRange *TimeRange) ([]*Event, return results, nil } -// FindByID returns the Error with the Wavefront-assigned ID. +// FindByID returns the Event with the Wavefront-assigned ID. // If not found an error is returned func (e Events) FindByID(id string) (*Event, error) { res, err := e.Find([]*SearchCondition{ diff --git a/vendor/github.com/spaceapegames/go-wavefront/target.go b/vendor/github.com/spaceapegames/go-wavefront/target.go index 1176538..08262e9 100644 --- a/vendor/github.com/spaceapegames/go-wavefront/target.go +++ b/vendor/github.com/spaceapegames/go-wavefront/target.go @@ -33,7 +33,7 @@ type Target struct { // EmailSubject is the subject of the email which will be sent for this Target // (EMAIL targets only) EmailSubject string `json:"emailSubject"` - + // IsHTMLContent is a boolean value for wavefront to add HTML Boilerplate // while using HTML Templates as email. // (EMAIL targets only) diff --git a/vendor/github.com/spaceapegames/go-wavefront/version b/vendor/github.com/spaceapegames/go-wavefront/version index 26ca594..9c6d629 100644 --- a/vendor/github.com/spaceapegames/go-wavefront/version +++ b/vendor/github.com/spaceapegames/go-wavefront/version @@ -1 +1 @@ -1.5.1 +1.6.1 diff --git a/vendor/modules.txt b/vendor/modules.txt index 6e8859d..3c2eb55 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -202,7 +202,7 @@ github.com/posener/complete github.com/posener/complete/cmd/install github.com/posener/complete/cmd github.com/posener/complete/match -# github.com/spaceapegames/go-wavefront v0.0.0-20190424105721-72e8eb185145 +# github.com/spaceapegames/go-wavefront v1.6.2 github.com/spaceapegames/go-wavefront # github.com/spf13/afero v1.2.1 github.com/spf13/afero diff --git a/wavefront/resource_alert.go b/wavefront/resource_alert.go index caa6276..8476bf6 100644 --- a/wavefront/resource_alert.go +++ b/wavefront/resource_alert.go @@ -26,11 +26,11 @@ func resourceAlert() *schema.Resource { "alert_type": { Type: schema.TypeString, Optional: true, - Default: wavefront.AlertTypeClassic, + Default: wavefront.AlertTypeClassic, }, "target": { Type: schema.TypeString, - Required: true, + Optional: true, }, "condition": { Type: schema.TypeString, @@ -38,12 +38,12 @@ func resourceAlert() *schema.Resource { StateFunc: trimSpaces, }, "threshold_conditions": { - Type: schema.TypeMap, - Optional: true, + Type: schema.TypeMap, + Optional: true, }, "threshold_targets": { - Type: schema.TypeMap, - Optional: true, + Type: schema.TypeMap, + Optional: true, }, "additional_information": { Type: schema.TypeString, @@ -84,44 +84,12 @@ func trimSpaces(d interface{}) string { return strings.TrimSpace(d.(string)) } -func mapToThresholdConditions(m map[string]string) (*wavefront.ThresholdConditions, error) { - var t wavefront.ThresholdConditions - for sev, ts := range m { - switch sev { - case "severe": - t.Severe = trimSpaces(ts) - case "warn": - t.Warn = trimSpaces(ts) - case "info": - t.Info = trimSpaces(ts) - case "smoke": - t.Smoke = trimSpaces(ts) - default: - return nil, fmt.Errorf("invalid severity: %s", sev) - } - } - - return &t, nil -} - -func mapToThresholdTargets(m map[string]string) (*wavefront.ThresholdTargets, error) { - var t wavefront.ThresholdTargets - for sev, ts := range m { - switch sev { - case "severe": - t.Severe = trimSpaces(ts) - case "warn": - t.Warn = trimSpaces(ts) - case "info": - t.Info = trimSpaces(ts) - case "smoke": - t.Smoke = trimSpaces(ts) - default: - return nil, fmt.Errorf("invalid severity: %s", sev) - } +func trimSpacesMap(m map[string]interface{}) map[string]string { + trimmed := map[string]string{} + for key, v := range m { + trimmed[key] = trimSpaces(v) } - - return &t, nil + return trimmed } func resourceAlertCreate(d *schema.ResourceData, m interface{}) error { @@ -132,9 +100,6 @@ func resourceAlertCreate(d *schema.ResourceData, m interface{}) error { tags = append(tags, tag.(string)) } - // Do some validation here? That we either have a condition or threshold_condition - // Also need to set the type accordinglt - a := &wavefront.Alert{ Name: d.Get("name").(string), AdditionalInfo: trimSpaces(d.Get("additional_information").(string)), @@ -145,34 +110,13 @@ func resourceAlertCreate(d *schema.ResourceData, m interface{}) error { Tags: tags, } - if d.Get("alert_type") == wavefront.AlertTypeThreshold { //DO THIS!! YOUVE CHANGED IT - a.Type = wavefront.AlertTypeThreshold - conditions, err := mapToThresholdConditions(d.Get("threshold_conditions").(map[string]string)) - if err != nil { - return err - } - a.ThresholdConditions = *conditions - - targets, err := mapToThresholdTargets(d.Get("threshold_conditions").(map[string]string)) - if err != nil { - return err - } - a.ThresholdTargets = *targets - } else if d.Get("condition") != nil { - // This is a CLASSIC alert - a.Type = wavefront.AlertTypeClassic - a.Condition = trimSpaces(d.Get("condition").(string)) - if d.Get("severity") == nil { - return fmt.Errorf("severity must be supplied with a classic alert") - } - a.Severity = d.Get("severity").(string) - a.Target = d.Get("target").(string) - } else { - return fmt.Errorf("either condition or threshold_conditions is required") + err := validateAlertConditions(a, d) + if err != nil { + return err } // Create the alert on Wavefront - err := alerts.Create(a) + err = alerts.Create(a) if err != nil { return fmt.Errorf("error creating Alert %s. %s", d.Get("name"), err) } @@ -208,7 +152,9 @@ func resourceAlertRead(d *schema.ResourceData, m interface{}) error { d.Set("notification_resend_frequency_minutes", tmpAlert.NotificationResendFrequencyMinutes) d.Set("severity", tmpAlert.Severity) d.Set("tags", tmpAlert.Tags) - d.Set("threshold_conditions", tmpAlert.ThresholdConditions) + d.Set("alert_type", tmpAlert.AlertType) + d.Set("threshold_conditions", tmpAlert.Conditions) + d.Set("threshold_targets", tmpAlert.Targets) return nil } @@ -231,16 +177,18 @@ func resourceAlertUpdate(d *schema.ResourceData, m interface{}) error { a := tmpAlert a.Name = d.Get("name").(string) - a.Target = d.Get("target").(string) - a.Condition = trimSpaces(d.Get("condition").(string)) a.AdditionalInfo = trimSpaces(d.Get("additional_information").(string)) a.DisplayExpression = trimSpaces(d.Get("display_expression").(string)) a.Minutes = d.Get("minutes").(int) a.ResolveAfterMinutes = d.Get("resolve_after_minutes").(int) a.NotificationResendFrequencyMinutes = d.Get("notification_resend_frequency_minutes").(int) - a.Severity = d.Get("severity").(string) a.Tags = tags + err = validateAlertConditions(&a, d) + if err != nil { + return err + } + // Update the alert on Wavefront err = alerts.Update(&a) if err != nil { @@ -268,3 +216,36 @@ func resourceAlertDelete(d *schema.ResourceData, m interface{}) error { d.SetId("") return nil } + +func validateAlertConditions(a *wavefront.Alert, d *schema.ResourceData) error { + if d.Get("alert_type") == wavefront.AlertTypeThreshold { + a.AlertType = wavefront.AlertTypeThreshold + if conditions, ok := d.GetOk("threshold_conditions"); ok { + a.Conditions = trimSpacesMap(conditions.(map[string]interface{})) + } else { + return fmt.Errorf("threshold_conditions must be supplied for threshold alerts") + } + + if targets, ok := d.GetOk("threshold_targets"); ok { + a.Targets = trimSpacesMap(targets.(map[string]interface{})) + } + + } else if d.Get("alert_type") == wavefront.AlertTypeClassic { + a.AlertType = wavefront.AlertTypeClassic + + if d.Get("condition") == "" { + return fmt.Errorf("condition must be supplied for classic alerts") + } + a.Condition = trimSpaces(d.Get("condition").(string)) + + if d.Get("severity") == "" { + return fmt.Errorf("severity must be supplied for classic alerts") + } + a.Severity = d.Get("severity").(string) + a.Target = d.Get("target").(string) + } else { + return fmt.Errorf("alert_type must be CLASSIC or THRESHOLD") + } + + return nil +} diff --git a/wavefront/resource_alert_test.go b/wavefront/resource_alert_test.go index 04176e3..da9b1ef 100644 --- a/wavefront/resource_alert_test.go +++ b/wavefront/resource_alert_test.go @@ -2,6 +2,8 @@ package wavefront_plugin import ( "fmt" + "github.com/hashicorp/terraform/helper/schema" + "os" "testing" "github.com/hashicorp/terraform/helper/resource" @@ -168,6 +170,160 @@ func TestAccWavefrontAlert_Multiple(t *testing.T) { }) } +func TestAccWavefrontAlert_Threshold(t *testing.T) { + var record wavefront.Alert + var tmpTarget *wavefront.Target + var err error + tmpTargetID := "" + + if os.Getenv("TF_ACC") == "true" { + tmpTarget, err = createTarget() + if err != nil { + t.Errorf("unable to create temp target: %s", err) + return + } + + tmpTargetID = *tmpTarget.ID + + defer deleteTarget(tmpTarget) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckWavefrontAlertDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckWavefrontAlert_threshold(tmpTargetID), + Check: resource.ComposeTestCheckFunc( + testAccCheckWavefrontAlertExists("wavefront_alert.test_threshold_alert", &record), + testAccCheckWavefrontThresholdAlertAttributes(&record, tmpTargetID), + + //Check against state that the attributes are as we expect + resource.TestCheckResourceAttr( + "wavefront_alert.test_threshold_alert", "threshold_conditions.%", "3"), + resource.TestCheckResourceAttr( + "wavefront_alert.test_threshold_alert", "threshold_targets.%", "1"), + ), + }, + }, + }) +} + +func TestResourceAlert_validateAlertConditions(t *testing.T) { + + cases := []struct { + name string + conf *schema.ResourceData + errorMessage string + }{ + { + "invalid alert type", + func() *schema.ResourceData { + d := resourceAlert().TestResourceData() + d.Set("alert_type", "WRONG") + return d + }(), + "alert_type must be CLASSIC or THRESHOLD", + }, + { + "classic alert missing condition", + func() *schema.ResourceData { + d := resourceAlert().TestResourceData() + d.Set("alert_type", "CLASSIC") + d.Set("severity", "severe") + return d + }(), + "condition must be supplied for classic alerts", + }, + { + "classic alert missing severity", + func() *schema.ResourceData { + d := resourceAlert().TestResourceData() + d.Set("alert_type", "CLASSIC") + d.Set("condition", "ts()") + return d + }(), + "severity must be supplied for classic alerts", + }, + { + "classic alert", + func() *schema.ResourceData { + d := resourceAlert().TestResourceData() + d.Set("alert_type", "CLASSIC") + d.Set("condition", "ts()") + d.Set("severity", "severe") + return d + }(), + "", + }, + { + "threshold alert missing conditions", + func() *schema.ResourceData { + d := resourceAlert().TestResourceData() + d.Set("alert_type", "THRESHOLD") + return d + }(), + "threshold_conditions must be supplied for threshold alerts", + }, + { + "threshold alert", + func() *schema.ResourceData { + d := resourceAlert().TestResourceData() + d.Set("alert_type", "THRESHOLD") + d.Set("threshold_conditions", map[string]interface{}{"severe": "ts()"}) + return d + }(), + "", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := validateAlertConditions(&wavefront.Alert{}, c.conf) + + m := "" + if err == nil { + m = "" + } else { + m = err.Error() + } + + if m != c.errorMessage { + t.Errorf("expected error '%s', got '%s'", c.errorMessage, err.Error()) + } + }) + } +} + +func createTarget() (*wavefront.Target, error) { + wt := wavefront.Target{ + Title: "test target", + Description: "testing threshols alert", + Method: "WEBHOOK", + Recipient: "https://hooks.slack.com/services/test/me", + ContentType: "application/json", + CustomHeaders: map[string]string{ + "Testing": "true", + }, + Triggers: []string{"ALERT_OPENED", "ALERT_RESOLVED"}, + Template: "{}", + } + + targets := testAccProvider.Meta().(*wavefrontClient).client.Targets() + + err := targets.Create(&wt) + if err != nil { + return nil, err + } + + return &wt, nil +} + +func deleteTarget(wt *wavefront.Target) { + testAccProvider.Meta().(*wavefrontClient).client.Targets().Delete(wt) +} + func testAccCheckWavefrontAlertDestroy(s *terraform.State) error { alerts := testAccProvider.Meta().(*wavefrontClient).client.Alerts() @@ -199,6 +355,21 @@ func testAccCheckWavefrontAlertAttributes(alert *wavefront.Alert) resource.TestC } } +func testAccCheckWavefrontThresholdAlertAttributes(alert *wavefront.Alert, targetID string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if val, ok := alert.Targets["severe"]; ok { + if val != "target:"+targetID { + return fmt.Errorf("bad value: %s", alert.Targets["severe"]) + } + } else { + return fmt.Errorf("target not set") + } + + return nil + } +} + func testAccCheckWavefrontAlertAttributesUpdated(alert *wavefront.Alert) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -395,3 +566,30 @@ resource "wavefront_alert" "test_alert3" { } `) } + +func testAccCheckWavefrontAlert_threshold(targetID string) string { + return fmt.Sprintf(` +resource "wavefront_alert" "test_threshold_alert" { + name = "Terraform Test Alert" + alert_type = "THRESHOLD" + additional_information = "This is a Terraform Test Alert" + display_expression = "100-ts(\"cpu.usage_idle\", environment=preprod and cpu=cpu-total )" + minutes = 5 + resolve_after_minutes = 5 + + threshold_conditions = { + "severe" = "100-ts(\"cpu.usage_idle\", environment=preprod and cpu=cpu-total ) > 80" + "warn" = "100-ts(\"cpu.usage_idle\", environment=preprod and cpu=cpu-total ) > 60" + "info" = "100-ts(\"cpu.usage_idle\", environment=preprod and cpu=cpu-total ) > 50" + } + + threshold_targets = { + "severe" = "target:%s" + } + + tags = [ + "terraform" + ] +} +`, targetID) +} From 55bfa37344d7050930ae4eba594de2078b2b7234 Mon Sep 17 00:00:00 2001 From: Louis McCormack Date: Tue, 2 Jul 2019 18:02:07 +0100 Subject: [PATCH 3/4] move alert creation inside config, add severity checks --- wavefront/resource_alert.go | 20 +++++++ wavefront/resource_alert_test.go | 99 +++++++++++++++----------------- 2 files changed, 66 insertions(+), 53 deletions(-) diff --git a/wavefront/resource_alert.go b/wavefront/resource_alert.go index 8476bf6..f90e1d1 100644 --- a/wavefront/resource_alert.go +++ b/wavefront/resource_alert.go @@ -222,12 +222,17 @@ func validateAlertConditions(a *wavefront.Alert, d *schema.ResourceData) error { a.AlertType = wavefront.AlertTypeThreshold if conditions, ok := d.GetOk("threshold_conditions"); ok { a.Conditions = trimSpacesMap(conditions.(map[string]interface{})) + err := validateThresholdLevels(a.Conditions) + if err != nil { + return err + } } else { return fmt.Errorf("threshold_conditions must be supplied for threshold alerts") } if targets, ok := d.GetOk("threshold_targets"); ok { a.Targets = trimSpacesMap(targets.(map[string]interface{})) + return validateThresholdLevels(a.Targets) } } else if d.Get("alert_type") == wavefront.AlertTypeClassic { @@ -249,3 +254,18 @@ func validateAlertConditions(a *wavefront.Alert, d *schema.ResourceData) error { return nil } + +func validateThresholdLevels(m map[string]string) error { + for key := range m { + ok := false + for _, level := range []string{"severe", "warn", "info", "smoke"} { + if key == level { + ok = true + } + } + if !ok { + return fmt.Errorf("invalid severity: %s", key) + } + } + return nil +} diff --git a/wavefront/resource_alert_test.go b/wavefront/resource_alert_test.go index da9b1ef..76dff20 100644 --- a/wavefront/resource_alert_test.go +++ b/wavefront/resource_alert_test.go @@ -3,7 +3,6 @@ package wavefront_plugin import ( "fmt" "github.com/hashicorp/terraform/helper/schema" - "os" "testing" "github.com/hashicorp/terraform/helper/resource" @@ -172,21 +171,6 @@ func TestAccWavefrontAlert_Multiple(t *testing.T) { func TestAccWavefrontAlert_Threshold(t *testing.T) { var record wavefront.Alert - var tmpTarget *wavefront.Target - var err error - tmpTargetID := "" - - if os.Getenv("TF_ACC") == "true" { - tmpTarget, err = createTarget() - if err != nil { - t.Errorf("unable to create temp target: %s", err) - return - } - - tmpTargetID = *tmpTarget.ID - - defer deleteTarget(tmpTarget) - } resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -194,10 +178,10 @@ func TestAccWavefrontAlert_Threshold(t *testing.T) { CheckDestroy: testAccCheckWavefrontAlertDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckWavefrontAlert_threshold(tmpTargetID), + Config: testAccCheckWavefrontAlert_threshold(), Check: resource.ComposeTestCheckFunc( testAccCheckWavefrontAlertExists("wavefront_alert.test_threshold_alert", &record), - testAccCheckWavefrontThresholdAlertAttributes(&record, tmpTargetID), + testAccCheckWavefrontThresholdAlertAttributes(&record), //Check against state that the attributes are as we expect resource.TestCheckResourceAttr( @@ -276,6 +260,28 @@ func TestResourceAlert_validateAlertConditions(t *testing.T) { }(), "", }, + { + "threshold alert invalid condition", + func() *schema.ResourceData { + d := resourceAlert().TestResourceData() + d.Set("alert_type", "THRESHOLD") + d.Set("threshold_conditions", map[string]interface{}{"banana": "ts()"}) + return d + }(), + "invalid severity: banana", + }, + { + "threshold alert invalid target", + func() *schema.ResourceData { + d := resourceAlert().TestResourceData() + d.Set("alert_type", "THRESHOLD") + d.Set("threshold_conditions", map[string]interface{}{"severe": "ts()"}) + d.Set("threshold_targets", map[string]interface{}{"banana": "ts()"}) + + return d + }(), + "invalid severity: banana", + }, } for _, c := range cases { @@ -296,34 +302,6 @@ func TestResourceAlert_validateAlertConditions(t *testing.T) { } } -func createTarget() (*wavefront.Target, error) { - wt := wavefront.Target{ - Title: "test target", - Description: "testing threshols alert", - Method: "WEBHOOK", - Recipient: "https://hooks.slack.com/services/test/me", - ContentType: "application/json", - CustomHeaders: map[string]string{ - "Testing": "true", - }, - Triggers: []string{"ALERT_OPENED", "ALERT_RESOLVED"}, - Template: "{}", - } - - targets := testAccProvider.Meta().(*wavefrontClient).client.Targets() - - err := targets.Create(&wt) - if err != nil { - return nil, err - } - - return &wt, nil -} - -func deleteTarget(wt *wavefront.Target) { - testAccProvider.Meta().(*wavefrontClient).client.Targets().Delete(wt) -} - func testAccCheckWavefrontAlertDestroy(s *terraform.State) error { alerts := testAccProvider.Meta().(*wavefrontClient).client.Alerts() @@ -355,12 +333,12 @@ func testAccCheckWavefrontAlertAttributes(alert *wavefront.Alert) resource.TestC } } -func testAccCheckWavefrontThresholdAlertAttributes(alert *wavefront.Alert, targetID string) resource.TestCheckFunc { +func testAccCheckWavefrontThresholdAlertAttributes(alert *wavefront.Alert) resource.TestCheckFunc { return func(s *terraform.State) error { - if val, ok := alert.Targets["severe"]; ok { - if val != "target:"+targetID { - return fmt.Errorf("bad value: %s", alert.Targets["severe"]) + if val, ok := alert.Conditions["severe"]; ok { + if val != "100-ts(\"cpu.usage_idle\", environment=preprod and cpu=cpu-total ) > 80" { + return fmt.Errorf("bad value: %s", alert.Conditions["severe"]) } } else { return fmt.Errorf("target not set") @@ -567,8 +545,23 @@ resource "wavefront_alert" "test_alert3" { `) } -func testAccCheckWavefrontAlert_threshold(targetID string) string { +func testAccCheckWavefrontAlert_threshold() string { return fmt.Sprintf(` +resource "wavefront_alert_target" "test_target" { + name = "Terraform Test Target" + description = "Test target" + method = "EMAIL" + recipient = "test@example.com" + email_subject = "This is a test" + is_html_content = true + template = "{}" + triggers = [ + "ALERT_OPENED", + "ALERT_RESOLVED" + ] +} + + resource "wavefront_alert" "test_threshold_alert" { name = "Terraform Test Alert" alert_type = "THRESHOLD" @@ -584,12 +577,12 @@ resource "wavefront_alert" "test_threshold_alert" { } threshold_targets = { - "severe" = "target:%s" + "severe" = "target:${wavefront_alert_target.test_target.id}" } tags = [ "terraform" ] } -`, targetID) +`) } From 8495c7281abcb981f089b7908d2871698cae6a7c Mon Sep 17 00:00:00 2001 From: Louis McCormack Date: Wed, 3 Jul 2019 10:19:58 +0100 Subject: [PATCH 4/4] break from loop --- wavefront/resource_alert.go | 1 + 1 file changed, 1 insertion(+) diff --git a/wavefront/resource_alert.go b/wavefront/resource_alert.go index f90e1d1..108f19c 100644 --- a/wavefront/resource_alert.go +++ b/wavefront/resource_alert.go @@ -261,6 +261,7 @@ func validateThresholdLevels(m map[string]string) error { for _, level := range []string{"severe", "warn", "info", "smoke"} { if key == level { ok = true + break } } if !ok {