From 12b4c26c8c34d8aa39191c62a2fa1003bd93d825 Mon Sep 17 00:00:00 2001 From: solarchad <50369843+solarchad@users.noreply.github.com> Date: Tue, 24 Sep 2019 16:41:05 -0400 Subject: [PATCH] GS-486: Alerts Resource (#14) * Make alert resource compatible with new appoptics SDK * Rename alert resource file from librato * Fix tests * Fix type references * Update alert resource tests for new SDK * Move alert test to use appoptics naming * Change variable names * Add condition block when creating any test alerts (it's required) * Fix services data structure to be valid * Use proper type for returned ID * Apply previous fix in other function * Fix types and flesh out tests * Update tests to include tag block on alert resource creaetion --- appoptics/resource_appoptics_alert.go | 534 ++++++++++++++++++ ...st.go => resource_appoptics_alert_test.go} | 188 +++--- appoptics/resource_librato_alert.go | 480 ---------------- .../resource_librato_space_chart_test.go | 2 +- 4 files changed, 643 insertions(+), 561 deletions(-) create mode 100644 appoptics/resource_appoptics_alert.go rename appoptics/{resource_librato_alert_test.go => resource_appoptics_alert_test.go} (57%) delete mode 100644 appoptics/resource_librato_alert.go diff --git a/appoptics/resource_appoptics_alert.go b/appoptics/resource_appoptics_alert.go new file mode 100644 index 0000000..b3aa3ad --- /dev/null +++ b/appoptics/resource_appoptics_alert.go @@ -0,0 +1,534 @@ +package appoptics + +import ( + "bytes" + "fmt" + "log" + "math" + "strconv" + "time" + + "github.com/appoptics/appoptics-api-go" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAppOpticsAlert() *schema.Resource { + return &schema.Resource{ + Create: resourceAppOpticsAlertCreate, + Read: resourceAppOpticsAlertRead, + Update: resourceAppOpticsAlertUpdate, + Delete: resourceAppOpticsAlertDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "rearm_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 600, + }, + "services": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "condition": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + }, + "metric_name": { + Type: schema.TypeString, + Optional: true, + }, + "tag": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "grouped": { + Type: schema.TypeBool, + Optional: true, + }, + "values": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "detect_reset": { + Type: schema.TypeBool, + Optional: true, + }, + "duration": { + Type: schema.TypeInt, + Optional: true, + }, + "threshold": { + Type: schema.TypeFloat, + Optional: true, + }, + "summary_function": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + Set: resourceAppOpticsAlertConditionsHash, + }, + "attributes": { + Type: schema.TypeMap, + Optional: true, + }, + }, + } +} + +func resourceAppOpticsAlertConditionsHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["type"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["metric_name"].(string))) + + tags, present := m["tag"].([]interface{}) + if present && len(tags) > 0 { + buf.WriteString(fmt.Sprintf("%d-", tagsHash(tags))) + } + + detectReset, present := m["detect_reset"] + if present { + buf.WriteString(fmt.Sprintf("%t-", detectReset.(bool))) + } + + duration, present := m["duration"] + if present { + buf.WriteString(fmt.Sprintf("%d-", duration.(int))) + } + + threshold, present := m["threshold"] + if present { + buf.WriteString(fmt.Sprintf("%f-", threshold.(float64))) + } + + summaryFunction, present := m["summary_function"] + if present { + buf.WriteString(fmt.Sprintf("%s-", summaryFunction.(string))) + } + + return hashcode.String(buf.String()) +} + +func tagsHash(tags []interface{}) int { + var buf bytes.Buffer + for _, v := range tags { + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["name"])) + buf.WriteString(fmt.Sprintf("%s-", m["grouped"])) + buf.WriteString(fmt.Sprintf("%d-", tagsValuesHash(m["values"].([]interface{})))) + } + + return hashcode.String(buf.String()) +} + +func tagsValuesHash(s []interface{}) int { + var buf bytes.Buffer + for _, v := range s { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + + return hashcode.String(buf.String()) +} + +func resourceAppOpticsAlertCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*appoptics.Client) + + alert := appoptics.Alert{ + Name: d.Get("name").(string), + } + if v, ok := d.GetOk("description"); ok { + alert.Description = v.(string) + } + // GetOK returns not OK for false boolean values, use Get + alert.Active = d.Get("active").(bool) + if v, ok := d.GetOk("rearm_seconds"); ok { + alert.RearmSeconds = v.(int) + } + if v, ok := d.GetOk("services"); ok { + vs := v.(*schema.Set) + services := make([]*appoptics.Service, vs.Len()) + for i, serviceID := range vs.List() { + service := new(appoptics.Service) + var err error + service.ID, err = strconv.Atoi(serviceID.(string)) + if err != nil { + return err + } + services[i] = service + } + alert.Services = services + } + if v, ok := d.GetOk("condition"); ok { + vs := v.(*schema.Set) + conditions := make([]*appoptics.AlertCondition, vs.Len()) + + for i, conditionDataM := range vs.List() { + conditionData := conditionDataM.(map[string]interface{}) + condition := appoptics.AlertCondition{} + + if v, ok := conditionData["type"].(string); ok && v != "" { + condition.Type = v + } + if v, ok := conditionData["threshold"].(float64); ok && !math.IsNaN(v) { + condition.Threshold = v + } + if v, ok := conditionData["metric_name"].(string); ok && v != "" { + condition.MetricName = v + } + if v, ok := conditionData["tag"].([]interface{}); ok { + tags := make([]*appoptics.Tag, len(v)) + for i, tagData := range v { + tag := appoptics.Tag{} + tag.Grouped = tagData.(map[string]interface{})["grouped"].(bool) + tag.Name = tagData.(map[string]interface{})["name"].(string) + values := tagData.(map[string]interface{})["values"].([]interface{}) + valuesInStrings := make([]string, len(values)) + for i, v := range values { + valuesInStrings[i] = v.(string) + } + tag.Values = valuesInStrings + tags[i] = &tag + } + + condition.Tags = tags + } + if v, ok := conditionData["duration"].(int); ok { + condition.Duration = v + } + if v, ok := conditionData["summary_function"].(string); ok && v != "" { + condition.SummaryFunction = v + } + conditions[i] = &condition + } + + alert.Conditions = conditions + } + if v, ok := d.GetOk("attributes"); ok { + attributeData := v.(map[string]interface{}) + if len(attributeData) > 1 { + return fmt.Errorf("Only one set of attributes per alert is supported") + } else if len(attributeData) == 1 { + // The only attribute here should be the runbook_url + alert.Attributes = attributeData + } + } + + alertResult, err := client.AlertsService().Create(&alert) + + if err != nil { + return fmt.Errorf("Error creating AppOptics alert %s: %s", alert.Name, err) + } + log.Printf("[INFO] Created AppOptics alert: %s", alertResult.Name) + + retryErr := resource.Retry(1*time.Minute, func() *resource.RetryError { + _, err := client.AlertsService().Retrieve(alertResult.ID) + if err != nil { + if errResp, ok := err.(*appoptics.ErrorResponse); ok && errResp.Response.StatusCode == 404 { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + if retryErr != nil { + return fmt.Errorf("Error creating AppOptics alert %s: %s", alert.Name, err) + } + + d.SetId(strconv.FormatUint(uint64(alertResult.ID), 10)) + + return resourceAppOpticsAlertRead(d, meta) +} + +func resourceAppOpticsAlertRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*appoptics.Client) + id, err := strconv.ParseUint(d.Id(), 10, 0) + if err != nil { + return err + } + + log.Printf("[INFO] Reading AppOptics Alert: %d", id) + alert, err := client.AlertsService().Retrieve(int(id)) + if err != nil { + if errResp, ok := err.(*appoptics.ErrorResponse); ok && errResp.Response.StatusCode == 404 { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading AppOptics Alert %s: %s", d.Id(), err) + } + log.Printf("[INFO] Received AppOptics Alert: %s", alert.Name) + + d.Set("name", alert.Name) + + if err := d.Set("description", alert.Description); err != nil { + return err + } + + if err := d.Set("active", alert.Active); err != nil { + return err + } + + if err := d.Set("rearm_seconds", alert.RearmSeconds); err != nil { + return err + } + + // Since the following aren't simple terraform types (TypeList), it's best to + // catch the error returned from the d.Set() function, and handle accordingly. + services := flattenServices(d, alert.Services) + // TODO: does this need `schema.NewSet(...)`? + if err := d.Set("services", schema.NewSet(schema.HashString, services)); err != nil { + return err + } + + conditions := flattenCondition(d, alert.Conditions) + if err := d.Set("condition", conditions); err != nil { + return err + } + + if err := d.Set("attributes", alert.Attributes); err != nil { + return err + } + + return nil +} + +func flattenServices(d *schema.ResourceData, services []*appoptics.Service) []interface{} { + retServices := make([]interface{}, 0, len(services)) + + for _, serviceData := range services { + retServices = append(retServices, fmt.Sprintf("%.d", serviceData.ID)) + } + + return retServices +} + +func flattenCondition(d *schema.ResourceData, conditions []*appoptics.AlertCondition) []interface{} { + out := make([]interface{}, 0, len(conditions)) + for _, c := range conditions { + condition := make(map[string]interface{}) + condition["type"] = c.Type + condition["threshold"] = c.Threshold + condition["metric_name"] = c.MetricName + condition["tag"] = flattenConditionTags(c.Tags) + // TODO: once we upgrade the appoptics-api-go dependency, + // we need to add a `condition["detect_reset"] = c.DetectReset` below + // SEE: https://github.com/appoptics/terraform-provider-appoptics/issues/12 + // condition["detect_reset"] = c.DetectReset + condition["duration"] = int(c.Duration) + condition["summary_function"] = c.SummaryFunction + out = append(out, condition) + } + + return out +} + +func flattenConditionTags(in []*appoptics.Tag) []interface{} { + var out = make([]interface{}, 0, len(in)) + for _, v := range in { + m := make(map[string]interface{}) + m["name"] = v.Name + m["grouped"] = v.Grouped + if len(v.Values) > 0 { + m["values"] = flattenConditionTagsValues(v.Values) + } + out = append(out, m) + } + + return out +} + +func flattenConditionTagsValues(in []string) []interface{} { + out := make([]interface{}, 0, len(in)) + for _, v := range in { + out = append(out, v) + } + return out +} + +func resourceAppOpticsAlertUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*appoptics.Client) + + id, err := strconv.ParseInt(d.Id(), 10, 0) + if err != nil { + return err + } + + alert := new(appoptics.Alert) + alert.ID = int(id) + alert.Name = d.Get("name").(string) + + if d.HasChange("description") { + alert.Description = d.Get("description").(string) + } + if d.HasChange("active") { + alert.Active = d.Get("active").(bool) + } + if d.HasChange("rearm_seconds") { + alert.RearmSeconds = d.Get("rearm_seconds").(int) + } + if d.HasChange("services") { + vs := d.Get("services").(*schema.Set) + services := make([]*appoptics.Service, vs.Len()) + for i, serviceID := range vs.List() { + service := new(appoptics.Service) + var err error + service.ID, err = strconv.Atoi(serviceID.(string)) + if err != nil { + return err + } + services[i] = service + } + alert.Services = services + } + + // We always have to send the conditions hash, from the API docs: + // + // NOTE: This method requires the conditions hash. + // If conditions is not included in the payload, the alert conditions will be removed. + vs := d.Get("condition").(*schema.Set) + conditions := make([]*appoptics.AlertCondition, vs.Len()) + + for i, conditionDataM := range vs.List() { + conditionData := conditionDataM.(map[string]interface{}) + condition := appoptics.AlertCondition{} + + if v, ok := conditionData["type"].(string); ok && v != "" { + condition.Type = v + } + if v, ok := conditionData["threshold"].(float64); ok && !math.IsNaN(v) { + condition.Threshold = v + } + if v, ok := conditionData["metric_name"].(string); ok && v != "" { + condition.MetricName = v + } + if v, ok := conditionData["tag"].([]interface{}); ok { + tags := make([]*appoptics.Tag, len(v)) + for i, tagData := range v { + tag := appoptics.Tag{} + tag.Grouped = tagData.(map[string]interface{})["grouped"].(bool) + tag.Name = tagData.(map[string]interface{})["name"].(string) + values := tagData.(map[string]interface{})["values"].([]interface{}) + valuesInStrings := make([]string, len(values)) + for i, v := range values { + valuesInStrings[i] = v.(string) + } + tag.Values = valuesInStrings + tags[i] = &tag + } + + condition.Tags = tags + } + if v, ok := conditionData["duration"].(int); ok { + condition.Duration = v + } + if v, ok := conditionData["summary_function"].(string); ok && v != "" { + condition.SummaryFunction = v + } + conditions[i] = &condition + } + alert.Conditions = conditions + + if d.HasChange("attributes") { + attributeData := d.Get("attributes").([]interface{}) + if attributeData[0] == nil { + return fmt.Errorf("No attributes found in attributes block") + } + + alert.Attributes = attributeData[0].(map[string]interface{}) + } + + log.Printf("[INFO] Updating AppOptics alert: %s", alert.Name) + updErr := client.AlertsService().Update(alert) + if updErr != nil { + return fmt.Errorf("Error updating AppOptics alert: %s", updErr) + } + + log.Printf("[INFO] Updated AppOptics alert %d", id) + + // Wait for propagation since AppOptics updates are eventually consistent + wait := resource.StateChangeConf{ + Pending: []string{fmt.Sprintf("%t", false)}, + Target: []string{fmt.Sprintf("%t", true)}, + Timeout: 5 * time.Minute, + MinTimeout: 2 * time.Second, + ContinuousTargetOccurence: 5, + Refresh: func() (interface{}, string, error) { + log.Printf("[DEBUG] Checking if AppOptics Alert %d was updated yet", id) + changedAlert, getErr := client.AlertsService().Retrieve(int(id)) + if getErr != nil { + return changedAlert, "", getErr + } + return changedAlert, "true", nil + }, + } + + _, err = wait.WaitForState() + if err != nil { + return fmt.Errorf("Failed updating AppOptics Alert %d: %s", id, err) + } + + return resourceAppOpticsAlertRead(d, meta) +} + +func resourceAppOpticsAlertDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*appoptics.Client) + id, err := strconv.ParseUint(d.Id(), 10, 0) + if err != nil { + return err + } + + log.Printf("[INFO] Deleting Alert: %d", id) + err = client.AlertsService().Delete(int(id)) + if err != nil { + return fmt.Errorf("Error deleting Alert: %s", err) + } + + retryErr := resource.Retry(1*time.Minute, func() *resource.RetryError { + _, err := client.AlertsService().Retrieve(int(id)) + if err != nil { + if errResp, ok := err.(*appoptics.ErrorResponse); ok && errResp.Response.StatusCode == 404 { + return nil + } + return resource.NonRetryableError(err) + } + return resource.RetryableError(fmt.Errorf("alert still exists")) + }) + if retryErr != nil { + return fmt.Errorf("Error deleting AppOptics alert: %s", err) + } + + return nil +} diff --git a/appoptics/resource_librato_alert_test.go b/appoptics/resource_appoptics_alert_test.go similarity index 57% rename from appoptics/resource_librato_alert_test.go rename to appoptics/resource_appoptics_alert_test.go index bfbf498..07e29bd 100644 --- a/appoptics/resource_librato_alert_test.go +++ b/appoptics/resource_appoptics_alert_test.go @@ -5,14 +5,14 @@ import ( "strconv" "testing" - "github.com/akahn/go-librato/librato" + "github.com/appoptics/appoptics-api-go" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAppOpticsAlert_Minimal(t *testing.T) { - var alert librato.Alert +func TestAccAppOpticsAlertMinimal(t *testing.T) { + var alert appoptics.Alert name := acctest.RandString(10) resource.Test(t, resource.TestCase{ @@ -21,20 +21,19 @@ func TestAccAppOpticsAlert_Minimal(t *testing.T) { CheckDestroy: testAccCheckAppOpticsAlertDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckAppOpticsAlertConfig_minimal(name), + Config: testAccCheckAppOpticsAlertConfigMinimal(name), Check: resource.ComposeTestCheckFunc( testAccCheckAppOpticsAlertExists("appoptics_alert.foobar", &alert), testAccCheckAppOpticsAlertName(&alert, name), - resource.TestCheckResourceAttr( - "appoptics_alert.foobar", "name", name), + resource.TestCheckResourceAttr("appoptics_alert.foobar", "name", name), ), }, }, }) } -func TestAccAppOpticsAlert_Basic(t *testing.T) { - var alert librato.Alert +func TestAccAppOpticsAlertBasic(t *testing.T) { + var alert appoptics.Alert name := acctest.RandString(10) resource.Test(t, resource.TestCase{ @@ -43,7 +42,7 @@ func TestAccAppOpticsAlert_Basic(t *testing.T) { CheckDestroy: testAccCheckAppOpticsAlertDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckAppOpticsAlertConfig_basic(name), + Config: testAccCheckAppOpticsAlertConfigBasic(name), Check: resource.ComposeTestCheckFunc( testAccCheckAppOpticsAlertExists("appoptics_alert.foobar", &alert), testAccCheckAppOpticsAlertName(&alert, name), @@ -56,8 +55,8 @@ func TestAccAppOpticsAlert_Basic(t *testing.T) { }) } -func TestAccAppOpticsAlert_Full(t *testing.T) { - var alert librato.Alert +func TestAccAppOpticsAlertFull(t *testing.T) { + var alert appoptics.Alert name := acctest.RandString(10) resource.Test(t, resource.TestCase{ @@ -66,7 +65,7 @@ func TestAccAppOpticsAlert_Full(t *testing.T) { CheckDestroy: testAccCheckAppOpticsAlertDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckAppOpticsAlertConfig_full(name), + Config: testAccCheckAppOpticsAlertConfigFull(name), Check: resource.ComposeTestCheckFunc( testAccCheckAppOpticsAlertExists("appoptics_alert.foobar", &alert), testAccCheckAppOpticsAlertName(&alert, name), @@ -74,21 +73,35 @@ func TestAccAppOpticsAlert_Full(t *testing.T) { resource.TestCheckResourceAttr( "appoptics_alert.foobar", "name", name), resource.TestCheckResourceAttr( - "appoptics_alert.foobar", "condition.836525194.metric_name", "librato.cpu.percent.idle"), + "appoptics_alert.foobar", "attributes.runbook_url", "https://www.youtube.com/watch?v=oHg5SJYRHA0"), + resource.TestCheckResourceAttr( + "appoptics_alert.foobar", "condition.411654007.metric_name", "system.cpu.utilization"), + resource.TestCheckResourceAttr( + "appoptics_alert.foobar", "condition.411654007.summary_function", ""), + resource.TestCheckResourceAttr( + "appoptics_alert.foobar", "condition.411654007.threshold", "10"), + resource.TestCheckResourceAttr( + "appoptics_alert.foobar", "condition.411654007.type", "above"), resource.TestCheckResourceAttr( - "appoptics_alert.foobar", "condition.836525194.type", "above"), + "appoptics_alert.foobar", "condition.411654007.tag.0.grouped", "true"), resource.TestCheckResourceAttr( - "appoptics_alert.foobar", "condition.836525194.threshold", "10"), + "appoptics_alert.foobar", "condition.411654007.tag.0.name", "hostname"), resource.TestCheckResourceAttr( - "appoptics_alert.foobar", "condition.836525194.duration", "600"), + "appoptics_alert.foobar", "condition.411654007.tag.0.values.#", "2"), + resource.TestCheckResourceAttr( + "appoptics_alert.foobar", "condition.411654007.tag.0.values.0", "host1"), + resource.TestCheckResourceAttr( + "appoptics_alert.foobar", "condition.411654007.tag.0.values.1", "host2"), + resource.TestCheckResourceAttr( + "appoptics_alert.foobar", "rearm_seconds", "300"), ), }, }, }) } -func TestAccAppOpticsAlert_Updated(t *testing.T) { - var alert librato.Alert +func TestAccAppOpticsAlertUpdated(t *testing.T) { + var alert appoptics.Alert name := acctest.RandString(10) resource.Test(t, resource.TestCase{ @@ -97,7 +110,7 @@ func TestAccAppOpticsAlert_Updated(t *testing.T) { CheckDestroy: testAccCheckAppOpticsAlertDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckAppOpticsAlertConfig_basic(name), + Config: testAccCheckAppOpticsAlertConfigBasic(name), Check: resource.ComposeTestCheckFunc( testAccCheckAppOpticsAlertExists("appoptics_alert.foobar", &alert), testAccCheckAppOpticsAlertDescription(&alert, "A Test Alert"), @@ -106,7 +119,7 @@ func TestAccAppOpticsAlert_Updated(t *testing.T) { ), }, { - Config: testAccCheckAppOpticsAlertConfig_new_value(name), + Config: testAccCheckAppOpticsAlertConfigNewValue(name), Check: resource.ComposeTestCheckFunc( testAccCheckAppOpticsAlertExists("appoptics_alert.foobar", &alert), testAccCheckAppOpticsAlertDescription(&alert, "A modified Test Alert"), @@ -118,8 +131,8 @@ func TestAccAppOpticsAlert_Updated(t *testing.T) { }) } -func TestAccAppOpticsAlert_Rename(t *testing.T) { - var alert librato.Alert +func TestAccAppOpticsAlertRename(t *testing.T) { + var alert appoptics.Alert name := acctest.RandString(10) newName := acctest.RandString(10) @@ -129,7 +142,7 @@ func TestAccAppOpticsAlert_Rename(t *testing.T) { CheckDestroy: testAccCheckAppOpticsAlertDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckAppOpticsAlertConfig_basic(name), + Config: testAccCheckAppOpticsAlertConfigBasic(name), Check: resource.ComposeTestCheckFunc( testAccCheckAppOpticsAlertExists("appoptics_alert.foobar", &alert), resource.TestCheckResourceAttr( @@ -137,7 +150,7 @@ func TestAccAppOpticsAlert_Rename(t *testing.T) { ), }, { - Config: testAccCheckAppOpticsAlertConfig_basic(newName), + Config: testAccCheckAppOpticsAlertConfigBasic(newName), Check: resource.ComposeTestCheckFunc( testAccCheckAppOpticsAlertExists("appoptics_alert.foobar", &alert), resource.TestCheckResourceAttr( @@ -148,8 +161,8 @@ func TestAccAppOpticsAlert_Rename(t *testing.T) { }) } -func TestAccAppOpticsAlert_FullUpdate(t *testing.T) { - var alert librato.Alert +func TestAccAppOpticsAlertFullUpdate(t *testing.T) { + var alert appoptics.Alert name := acctest.RandString(10) resource.Test(t, resource.TestCase{ @@ -158,7 +171,7 @@ func TestAccAppOpticsAlert_FullUpdate(t *testing.T) { CheckDestroy: testAccCheckAppOpticsAlertDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckAppOpticsAlertConfig_full_update(name), + Config: testAccCheckAppOpticsAlertConfigFullUpdate(name), Check: resource.ComposeTestCheckFunc( testAccCheckAppOpticsAlertExists("appoptics_alert.foobar", &alert), testAccCheckAppOpticsAlertName(&alert, name), @@ -168,13 +181,11 @@ func TestAccAppOpticsAlert_FullUpdate(t *testing.T) { resource.TestCheckResourceAttr( "appoptics_alert.foobar", "rearm_seconds", "1200"), resource.TestCheckResourceAttr( - "appoptics_alert.foobar", "condition.2524844643.metric_name", "librato.cpu.percent.idle"), + "appoptics_alert.foobar", "condition.498665064.metric_name", "system.cpu.utilization"), resource.TestCheckResourceAttr( - "appoptics_alert.foobar", "condition.2524844643.type", "above"), + "appoptics_alert.foobar", "condition.498665064.type", "above"), resource.TestCheckResourceAttr( - "appoptics_alert.foobar", "condition.2524844643.threshold", "10"), - resource.TestCheckResourceAttr( - "appoptics_alert.foobar", "condition.2524844643.duration", "60"), + "appoptics_alert.foobar", "condition.498665064.threshold", "10"), ), }, }, @@ -182,7 +193,7 @@ func TestAccAppOpticsAlert_FullUpdate(t *testing.T) { } func testAccCheckAppOpticsAlertDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*librato.Client) + client := testAccProvider.Meta().(*appoptics.Client) for _, rs := range s.RootModule().Resources { if rs.Type != "appoptics_alert" { @@ -194,7 +205,7 @@ func testAccCheckAppOpticsAlertDestroy(s *terraform.State) error { return fmt.Errorf("ID not a number") } - _, _, err = client.Alerts.Get(uint(id)) + _, err = client.AlertsService().Retrieve(int(id)) if err == nil { return fmt.Errorf("Alert still exists") @@ -204,29 +215,29 @@ func testAccCheckAppOpticsAlertDestroy(s *terraform.State) error { return nil } -func testAccCheckAppOpticsAlertName(alert *librato.Alert, name string) resource.TestCheckFunc { +func testAccCheckAppOpticsAlertName(alert *appoptics.Alert, name string) resource.TestCheckFunc { return func(s *terraform.State) error { - if alert.Name == nil || *alert.Name != name { - return fmt.Errorf("Bad name: %s", *alert.Name) + if alert.Name != name { + return fmt.Errorf("Bad name: %s", alert.Name) } return nil } } -func testAccCheckAppOpticsAlertDescription(alert *librato.Alert, description string) resource.TestCheckFunc { +func testAccCheckAppOpticsAlertDescription(alert *appoptics.Alert, description string) resource.TestCheckFunc { return func(s *terraform.State) error { - if alert.Description == nil || *alert.Description != description { - return fmt.Errorf("Bad description: %s", *alert.Description) + if alert.Description != description { + return fmt.Errorf("Bad description: %s", alert.Description) } return nil } } -func testAccCheckAppOpticsAlertExists(n string, alert *librato.Alert) resource.TestCheckFunc { +func testAccCheckAppOpticsAlertExists(n string, alert *appoptics.Alert) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -238,20 +249,20 @@ func testAccCheckAppOpticsAlertExists(n string, alert *librato.Alert) resource.T return fmt.Errorf("No Alert ID is set") } - client := testAccProvider.Meta().(*librato.Client) + client := testAccProvider.Meta().(*appoptics.Client) id, err := strconv.ParseUint(rs.Primary.ID, 10, 0) if err != nil { return fmt.Errorf("ID not a number") } - foundAlert, _, err := client.Alerts.Get(uint(id)) + foundAlert, err := client.AlertsService().Retrieve(int(id)) if err != nil { return err } - if foundAlert.ID == nil || *foundAlert.ID != uint(id) { + if foundAlert.ID == 0 { return fmt.Errorf("Alert not found") } @@ -261,30 +272,45 @@ func testAccCheckAppOpticsAlertExists(n string, alert *librato.Alert) resource.T } } -func testAccCheckAppOpticsAlertConfig_minimal(name string) string { +func testAccCheckAppOpticsAlertConfigMinimal(name string) string { return fmt.Sprintf(` resource "appoptics_alert" "foobar" { - name = "%s" + name = "%s" + condition { + type = "above" + threshold = 10 + metric_name = "system.cpu.utilization" + } }`, name) } -func testAccCheckAppOpticsAlertConfig_basic(name string) string { +func testAccCheckAppOpticsAlertConfigBasic(name string) string { return fmt.Sprintf(` resource "appoptics_alert" "foobar" { name = "%s" - description = "A Test Alert" + description = "A Test Alert" + condition { + type = "above" + threshold = 10 + metric_name = "system.cpu.utilization" + } }`, name) } -func testAccCheckAppOpticsAlertConfig_new_value(name string) string { +func testAccCheckAppOpticsAlertConfigNewValue(name string) string { return fmt.Sprintf(` resource "appoptics_alert" "foobar" { - name = "%s" - description = "A modified Test Alert" + name = "%s" + description = "A modified Test Alert" + condition { + type = "above" + threshold = 10 + metric_name = "system.cpu.utilization" + } }`, name) } -func testAccCheckAppOpticsAlertConfig_full(name string) string { +func testAccCheckAppOpticsAlertConfigFull(name string) string { return fmt.Sprintf(` resource "appoptics_service" "foobar" { title = "Foo Bar" @@ -297,24 +323,28 @@ EOF } resource "appoptics_alert" "foobar" { - name = "%s" - description = "A Test Alert" - services = [ "${appoptics_service.foobar.id}" ] - condition { - type = "above" - threshold = 10 - duration = 600 - metric_name = "librato.cpu.percent.idle" - } - attributes { - runbook_url = "https://www.youtube.com/watch?v=oHg5SJYRHA0" - } - active = false - rearm_seconds = 300 + name = "%s" + description = "A Test Alert" + services = [ "${appoptics_service.foobar.id}" ] + condition { + type = "above" + threshold = 10 + metric_name = "system.cpu.utilization" + + tag { + name = "hostname" + grouped = true + values = ["host1", "host2"] + } + } + attributes { + runbook_url = "https://www.youtube.com/watch?v=oHg5SJYRHA0" + } + rearm_seconds = 300 }`, name) } -func testAccCheckAppOpticsAlertConfig_full_update(name string) string { +func testAccCheckAppOpticsAlertConfigFullUpdate(name string) string { return fmt.Sprintf(` resource "appoptics_service" "foobar" { title = "Foo Bar" @@ -328,18 +358,16 @@ EOF resource "appoptics_alert" "foobar" { name = "%s" - description = "A Test Alert" - services = [ "${appoptics_service.foobar.id}" ] - condition { - type = "above" - threshold = 10 - duration = 60 - metric_name = "librato.cpu.percent.idle" - } - attributes { - runbook_url = "https://www.youtube.com/watch?v=oHg5SJYRHA0" - } - active = false - rearm_seconds = 1200 + description = "A Test Alert" + services = [ "${appoptics_service.foobar.id}" ] + condition { + type = "above" + threshold = 10 + metric_name = "system.cpu.utilization" + } + attributes { + runbook_url = "https://www.youtube.com/watch?v=oHg5SJYRHA0" + } + rearm_seconds = 1200 }`, name) } diff --git a/appoptics/resource_librato_alert.go b/appoptics/resource_librato_alert.go deleted file mode 100644 index a4fad62..0000000 --- a/appoptics/resource_librato_alert.go +++ /dev/null @@ -1,480 +0,0 @@ -package appoptics - -import ( - "bytes" - "fmt" - "log" - "math" - "strconv" - "time" - - "github.com/akahn/go-librato/librato" - "github.com/hashicorp/terraform/helper/hashcode" - "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/helper/schema" -) - -func resourceAppOpticsAlert() *schema.Resource { - return &schema.Resource{ - Create: resourceAppOpticsAlertCreate, - Read: resourceAppOpticsAlertRead, - Update: resourceAppOpticsAlertUpdate, - Delete: resourceAppOpticsAlertDelete, - - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: false, - }, - "description": { - Type: schema.TypeString, - Optional: true, - }, - "active": { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - "rearm_seconds": { - Type: schema.TypeInt, - Optional: true, - Default: 600, - }, - "services": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, - }, - "condition": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - }, - "metric_name": { - Type: schema.TypeString, - Required: true, - }, - "source": { - Type: schema.TypeString, - Optional: true, - }, - "detect_reset": { - Type: schema.TypeBool, - Optional: true, - }, - "duration": { - Type: schema.TypeInt, - Optional: true, - }, - "threshold": { - Type: schema.TypeFloat, - Optional: true, - }, - "summary_function": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - Set: resourceAppOpticsAlertConditionsHash, - }, - "attributes": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "runbook_url": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - }, - } -} - -func resourceAppOpticsAlertConditionsHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["type"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["metric_name"].(string))) - - source, present := m["source"] - if present { - buf.WriteString(fmt.Sprintf("%s-", source.(string))) - } - - detectReset, present := m["detect_reset"] - if present { - buf.WriteString(fmt.Sprintf("%t-", detectReset.(bool))) - } - - duration, present := m["duration"] - if present { - buf.WriteString(fmt.Sprintf("%d-", duration.(int))) - } - - threshold, present := m["threshold"] - if present { - buf.WriteString(fmt.Sprintf("%f-", threshold.(float64))) - } - - summaryFunction, present := m["summary_function"] - if present { - buf.WriteString(fmt.Sprintf("%s-", summaryFunction.(string))) - } - - return hashcode.String(buf.String()) -} - -func resourceAppOpticsAlertCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*librato.Client) - - alert := librato.Alert{ - Name: librato.String(d.Get("name").(string)), - } - if v, ok := d.GetOk("description"); ok { - alert.Description = librato.String(v.(string)) - } - // GetOK returns not OK for false boolean values, use Get - alert.Active = librato.Bool(d.Get("active").(bool)) - if v, ok := d.GetOk("rearm_seconds"); ok { - alert.RearmSeconds = librato.Uint(uint(v.(int))) - } - if v, ok := d.GetOk("services"); ok { - vs := v.(*schema.Set) - services := make([]*string, vs.Len()) - for i, serviceData := range vs.List() { - services[i] = librato.String(serviceData.(string)) - } - alert.Services = services - } - if v, ok := d.GetOk("condition"); ok { - vs := v.(*schema.Set) - conditions := make([]librato.AlertCondition, vs.Len()) - for i, conditionDataM := range vs.List() { - conditionData := conditionDataM.(map[string]interface{}) - var condition librato.AlertCondition - if v, ok := conditionData["type"].(string); ok && v != "" { - condition.Type = librato.String(v) - } - if v, ok := conditionData["threshold"].(float64); ok && !math.IsNaN(v) { - condition.Threshold = librato.Float(v) - } - if v, ok := conditionData["metric_name"].(string); ok && v != "" { - condition.MetricName = librato.String(v) - } - if v, ok := conditionData["source"].(string); ok && v != "" { - condition.Source = librato.String(v) - } - if v, ok := conditionData["detect_reset"].(bool); ok { - condition.DetectReset = librato.Bool(v) - } - if v, ok := conditionData["duration"].(int); ok { - condition.Duration = librato.Uint(uint(v)) - } - if v, ok := conditionData["summary_function"].(string); ok && v != "" { - condition.SummaryFunction = librato.String(v) - } - conditions[i] = condition - } - alert.Conditions = conditions - } - if v, ok := d.GetOk("attributes"); ok { - attributeData := v.([]interface{}) - if len(attributeData) > 1 { - return fmt.Errorf("Only one set of attributes per alert is supported") - } else if len(attributeData) == 1 { - if attributeData[0] == nil { - return fmt.Errorf("No attributes found in attributes block") - } - attributeDataMap := attributeData[0].(map[string]interface{}) - attributes := new(librato.AlertAttributes) - if v, ok := attributeDataMap["runbook_url"].(string); ok && v != "" { - attributes.RunbookURL = librato.String(v) - } - alert.Attributes = attributes - } - } - - alertResult, _, err := client.Alerts.Create(&alert) - - if err != nil { - return fmt.Errorf("Error creating AppOptics alert %s: %s", *alert.Name, err) - } - log.Printf("[INFO] Created AppOptics alert: %s", *alertResult) - - retryErr := resource.Retry(1*time.Minute, func() *resource.RetryError { - _, _, err := client.Alerts.Get(*alertResult.ID) - if err != nil { - if errResp, ok := err.(*librato.ErrorResponse); ok && errResp.Response.StatusCode == 404 { - return resource.RetryableError(err) - } - return resource.NonRetryableError(err) - } - return nil - }) - if retryErr != nil { - return fmt.Errorf("Error creating librato alert: %s", err) - } - - d.SetId(strconv.FormatUint(uint64(*alertResult.ID), 10)) - - return resourceAppOpticsAlertRead(d, meta) -} - -func resourceAppOpticsAlertRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*librato.Client) - id, err := strconv.ParseUint(d.Id(), 10, 0) - if err != nil { - return err - } - - log.Printf("[INFO] Reading AppOptics Alert: %d", id) - alert, _, err := client.Alerts.Get(uint(id)) - if err != nil { - if errResp, ok := err.(*librato.ErrorResponse); ok && errResp.Response.StatusCode == 404 { - d.SetId("") - return nil - } - return fmt.Errorf("Error reading AppOptics Alert %s: %s", d.Id(), err) - } - log.Printf("[INFO] Received AppOptics Alert: %s", *alert) - - d.Set("name", alert.Name) - - if alert.Description != nil { - if err := d.Set("description", alert.Description); err != nil { - return err - } - } - if alert.Active != nil { - if err := d.Set("active", alert.Active); err != nil { - return err - } - } - if alert.RearmSeconds != nil { - if err := d.Set("rearm_seconds", alert.RearmSeconds); err != nil { - return err - } - } - - // Since the following aren't simple terraform types (TypeList), it's best to - // catch the error returned from the d.Set() function, and handle accordingly. - services := resourceAppOpticsAlertServicesGather(d, alert.Services.([]interface{})) - if err := d.Set("services", schema.NewSet(schema.HashString, services)); err != nil { - return err - } - - conditions := resourceAppOpticsAlertConditionsGather(d, alert.Conditions) - if err := d.Set("condition", schema.NewSet(resourceAppOpticsAlertConditionsHash, conditions)); err != nil { - return err - } - - attributes := resourceAppOpticsAlertAttributesGather(d, alert.Attributes) - if err := d.Set("attributes", attributes); err != nil { - return err - } - - return nil -} - -func resourceAppOpticsAlertServicesGather(d *schema.ResourceData, services []interface{}) []interface{} { - retServices := make([]interface{}, 0, len(services)) - - for _, s := range services { - serviceData := s.(map[string]interface{}) - // ID field is returned as float64, for whatever reason - retServices = append(retServices, fmt.Sprintf("%.f", serviceData["id"])) - } - - return retServices -} - -func resourceAppOpticsAlertConditionsGather(d *schema.ResourceData, conditions []librato.AlertCondition) []interface{} { - retConditions := make([]interface{}, 0, len(conditions)) - for _, c := range conditions { - condition := make(map[string]interface{}) - if c.Type != nil { - condition["type"] = *c.Type - } - if c.Threshold != nil { - condition["threshold"] = *c.Threshold - } - if c.MetricName != nil { - condition["metric_name"] = *c.MetricName - } - if c.Source != nil { - condition["source"] = *c.Source - } - if c.DetectReset != nil { - condition["detect_reset"] = *c.MetricName - } - if c.Duration != nil { - condition["duration"] = int(*c.Duration) - } - if c.SummaryFunction != nil { - condition["summary_function"] = *c.SummaryFunction - } - retConditions = append(retConditions, condition) - } - - return retConditions -} - -// Flattens an attributes hash into something that flatmap.Flatten() can handle -func resourceAppOpticsAlertAttributesGather(d *schema.ResourceData, attributes *librato.AlertAttributes) []map[string]interface{} { - result := make([]map[string]interface{}, 0, 1) - - if attributes != nil { - retAttributes := make(map[string]interface{}) - if attributes.RunbookURL != nil { - retAttributes["runbook_url"] = *attributes.RunbookURL - } - result = append(result, retAttributes) - } - - return result -} - -func resourceAppOpticsAlertUpdate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*librato.Client) - - id, err := strconv.ParseUint(d.Id(), 10, 0) - if err != nil { - return err - } - - alert := new(librato.Alert) - alert.Name = librato.String(d.Get("name").(string)) - - if d.HasChange("description") { - alert.Description = librato.String(d.Get("description").(string)) - } - if d.HasChange("active") { - alert.Active = librato.Bool(d.Get("active").(bool)) - } - if d.HasChange("rearm_seconds") { - alert.RearmSeconds = librato.Uint(uint(d.Get("rearm_seconds").(int))) - } - if d.HasChange("services") { - vs := d.Get("services").(*schema.Set) - services := make([]*string, vs.Len()) - for i, serviceData := range vs.List() { - services[i] = librato.String(serviceData.(string)) - } - alert.Services = services - } - - vs := d.Get("condition").(*schema.Set) - conditions := make([]librato.AlertCondition, vs.Len()) - for i, conditionDataM := range vs.List() { - conditionData := conditionDataM.(map[string]interface{}) - var condition librato.AlertCondition - if v, ok := conditionData["type"].(string); ok && v != "" { - condition.Type = librato.String(v) - } - if v, ok := conditionData["threshold"].(float64); ok && !math.IsNaN(v) { - condition.Threshold = librato.Float(v) - } - if v, ok := conditionData["metric_name"].(string); ok && v != "" { - condition.MetricName = librato.String(v) - } - if v, ok := conditionData["source"].(string); ok && v != "" { - condition.Source = librato.String(v) - } - if v, ok := conditionData["detect_reset"].(bool); ok { - condition.DetectReset = librato.Bool(v) - } - if v, ok := conditionData["duration"].(int); ok { - condition.Duration = librato.Uint(uint(v)) - } - if v, ok := conditionData["summary_function"].(string); ok && v != "" { - condition.SummaryFunction = librato.String(v) - } - conditions[i] = condition - alert.Conditions = conditions - } - if d.HasChange("attributes") { - attributeData := d.Get("attributes").([]interface{}) - if attributeData[0] == nil { - return fmt.Errorf("No attributes found in attributes block") - } - attributeDataMap := attributeData[0].(map[string]interface{}) - attributes := new(librato.AlertAttributes) - if v, ok := attributeDataMap["runbook_url"].(string); ok && v != "" { - attributes.RunbookURL = librato.String(v) - } - alert.Attributes = attributes - } - - log.Printf("[INFO] Updating AppOptics alert: %s", alert) - _, updErr := client.Alerts.Update(uint(id), alert) - if updErr != nil { - return fmt.Errorf("Error updating AppOptics alert: %s", updErr) - } - - log.Printf("[INFO] Updated AppOptics alert %d", id) - - // Wait for propagation since AppOptics updates are eventually consistent - wait := resource.StateChangeConf{ - Pending: []string{fmt.Sprintf("%t", false)}, - Target: []string{fmt.Sprintf("%t", true)}, - Timeout: 5 * time.Minute, - MinTimeout: 2 * time.Second, - ContinuousTargetOccurence: 5, - Refresh: func() (interface{}, string, error) { - log.Printf("[DEBUG] Checking if AppOptics Alert %d was updated yet", id) - changedAlert, _, getErr := client.Alerts.Get(uint(id)) - if getErr != nil { - return changedAlert, "", getErr - } - return changedAlert, "true", nil - }, - } - - _, err = wait.WaitForState() - if err != nil { - return fmt.Errorf("Failed updating AppOptics Alert %d: %s", id, err) - } - - return resourceAppOpticsAlertRead(d, meta) -} - -func resourceAppOpticsAlertDelete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*librato.Client) - id, err := strconv.ParseUint(d.Id(), 10, 0) - if err != nil { - return err - } - - log.Printf("[INFO] Deleting Alert: %d", id) - _, err = client.Alerts.Delete(uint(id)) - if err != nil { - return fmt.Errorf("Error deleting Alert: %s", err) - } - - retryErr := resource.Retry(1*time.Minute, func() *resource.RetryError { - _, _, err := client.Alerts.Get(uint(id)) - if err != nil { - if errResp, ok := err.(*librato.ErrorResponse); ok && errResp.Response.StatusCode == 404 { - return nil - } - return resource.NonRetryableError(err) - } - return resource.RetryableError(fmt.Errorf("alert still exists")) - }) - if retryErr != nil { - return fmt.Errorf("Error deleting AppOptics alert: %s", err) - } - - return nil -} diff --git a/appoptics/resource_librato_space_chart_test.go b/appoptics/resource_librato_space_chart_test.go index 7e95632..25451ab 100644 --- a/appoptics/resource_librato_space_chart_test.go +++ b/appoptics/resource_librato_space_chart_test.go @@ -40,7 +40,7 @@ func TestAccAppOpticsSpaceChart_Full(t *testing.T) { CheckDestroy: testAccCheckAppOpticsSpaceChartDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccCheckAppOpticsSpaceChartConfig_full, + Config: testAccCheckAppOpticsSpaceChartConfigFull, Check: resource.ComposeTestCheckFunc( testAccCheckAppOpticsSpaceChartExists("appoptics_space_chart.foobar", &spaceChart), testAccCheckAppOpticsSpaceChartName(&spaceChart, "Foo Bar"),