From 5316739ba1418268387820b028d91817a0e7e6f8 Mon Sep 17 00:00:00 2001 From: Anthony Stanton Date: Fri, 12 Aug 2016 18:57:48 +0200 Subject: [PATCH 1/9] Update go-librato --- .../henrikhodne/go-librato/librato/alerts.go | 110 ++++++++++++ .../henrikhodne/go-librato/librato/client.go | 9 +- .../henrikhodne/go-librato/librato/metrics.go | 163 ++++++++++++++++++ .../go-librato/librato/pagination.go | 70 ++++++++ .../go-librato/librato/services.go | 90 ++++++++++ vendor/vendor.json | 5 +- 6 files changed, 444 insertions(+), 3 deletions(-) create mode 100644 vendor/github.com/henrikhodne/go-librato/librato/alerts.go create mode 100644 vendor/github.com/henrikhodne/go-librato/librato/metrics.go create mode 100644 vendor/github.com/henrikhodne/go-librato/librato/pagination.go create mode 100644 vendor/github.com/henrikhodne/go-librato/librato/services.go diff --git a/vendor/github.com/henrikhodne/go-librato/librato/alerts.go b/vendor/github.com/henrikhodne/go-librato/librato/alerts.go new file mode 100644 index 000000000..fd027b906 --- /dev/null +++ b/vendor/github.com/henrikhodne/go-librato/librato/alerts.go @@ -0,0 +1,110 @@ +package librato + +import ( + "fmt" + "net/http" +) + +// AlertsService handles communication with the Librato API methods related to +// alerts. +type AlertsService struct { + client *Client +} + +// Alert represents a Librato Alert. +type Alert struct { + Name *string `json:"name"` + ID *uint `json:"id,omitempty"` + Conditions []AlertCondition `json:"conditions,omitempty"` + // These are interface{} because the Librato API asks for integers + // on Create and returns hashes on Get + Services interface{} `json:"services,omitempty"` + Attributes *AlertAttributes `json:"attributes,omitempty"` + Description *string `json:"description,omitempty"` + Active *bool `json:"active,omitempty"` + RearmSeconds *uint `json:"rearm_seconds,omitempty"` +} + +func (a Alert) String() string { + return Stringify(a) +} + +// AlertCondition represents an alert trigger condition. +type AlertCondition struct { + Type *string `json:"type,omitempty"` + MetricName *string `json:"metric_name,omitempty"` + Source *string `json:"source,omitempty"` + DetectReset *bool `json:"detect_reset,omitempty"` + Threshold *float64 `json:"threshold,omitempty"` + SummaryFunction *string `json:"summary_function,omitempty"` + Duration *uint `json:"duration,omitempty"` +} + +// AlertAttributes represents the attributes of an alert. +type AlertAttributes struct { + RunbookURL *string `json:"runbook_url,omitempty"` +} + +// Get an alert by ID +// +// Librato API docs: https://www.librato.com/docs/api/#retrieve-alert-by-id +func (a *AlertsService) Get(id uint) (*Alert, *http.Response, error) { + urlStr := fmt.Sprintf("alerts/%d", id) + + req, err := a.client.NewRequest("GET", urlStr, nil) + if err != nil { + return nil, nil, err + } + + alert := new(Alert) + resp, err := a.client.Do(req, alert) + if err != nil { + return nil, resp, err + } + + return alert, resp, err +} + +// Create an alert +// +// Librato API docs: https://www.librato.com/docs/api/?shell#create-an-alert +func (a *AlertsService) Create(alert *Alert) (*Alert, *http.Response, error) { + req, err := a.client.NewRequest("POST", "alerts", alert) + if err != nil { + return nil, nil, err + } + + al := new(Alert) + resp, err := a.client.Do(req, al) + if err != nil { + return nil, resp, err + } + + return al, resp, err +} + +// Edit an alert. +// +// Librato API docs: https://www.librato.com/docs/api/?shell#update-alert +func (a *AlertsService) Edit(alertID uint, alert *Alert) (*http.Response, error) { + u := fmt.Sprintf("alerts/%d", alertID) + req, err := a.client.NewRequest("PUT", u, alert) + if err != nil { + return nil, err + } + + return a.client.Do(req, nil) +} + +// Delete an alert +// +// Librato API docs: https://www.librato.com/docs/api/?shell#delete-alert +func (a *AlertsService) Delete(id uint) (*http.Response, error) { + u := fmt.Sprintf("alerts/%d", id) + req, err := a.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + return a.client.Do(req, nil) +} diff --git a/vendor/github.com/henrikhodne/go-librato/librato/client.go b/vendor/github.com/henrikhodne/go-librato/librato/client.go index 181e2c7f5..5cb240c2d 100644 --- a/vendor/github.com/henrikhodne/go-librato/librato/client.go +++ b/vendor/github.com/henrikhodne/go-librato/librato/client.go @@ -44,7 +44,10 @@ type Client struct { UserAgent string // Services used to manipulate API entities. - Spaces *SpacesService + Spaces *SpacesService + Metrics *MetricsService + Alerts *AlertsService + Services *ServicesService } // NewClient returns a new Librato API client bound to the public Librato API. @@ -74,6 +77,9 @@ func NewClientWithBaseURL(baseURL *url.URL, email, token string) *Client { } c.Spaces = &SpacesService{client: c} + c.Metrics = &MetricsService{client: c} + c.Alerts = &AlertsService{client: c} + c.Services = &ServicesService{client: c} return c } @@ -89,7 +95,6 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ if err != nil { return nil, err } - u := c.BaseURL.ResolveReference(rel) var buf io.ReadWriter diff --git a/vendor/github.com/henrikhodne/go-librato/librato/metrics.go b/vendor/github.com/henrikhodne/go-librato/librato/metrics.go new file mode 100644 index 000000000..974204a29 --- /dev/null +++ b/vendor/github.com/henrikhodne/go-librato/librato/metrics.go @@ -0,0 +1,163 @@ +package librato + +import ( + "fmt" + "net/http" +) + +// MetricsService handles communication with the Librato API methods related to +// metrics. +type MetricsService struct { + client *Client +} + +// Metric represents a Librato Metric. +type Metric struct { + Name *string `json:"name"` + Period *uint `json:"period,omitempty"` + DisplayName *string `json:"display_name,omitempty"` + Attributes *MetricAttributes `json:"attributes,omitempty"` +} + +type MetricAttributes struct { + Color *string `json:"color"` + // These are interface{} because sometimes the Librato API + // returns strings, and sometimes it returns integers + DisplayMax interface{} `json:"display_max"` + DisplayMin interface{} `json:"display_min"` + DisplayUnitsShort string `json:"display_units_short"` + DisplayStacked bool `json:"display_stacked"` + DisplayTransform string `json:"display_transform"` +} + +type ListMetricsOptions struct { + *PaginationMeta + Name string `url:"name,omitempty"` +} + +// Advance to the specified page in result set, while retaining +// the filtering options. +func (l *ListMetricsOptions) AdvancePage(next *PaginationMeta) ListMetricsOptions { + return ListMetricsOptions{ + PaginationMeta: next, + Name: l.Name, + } +} + +type ListMetricsResponse struct { + ThisPage *PaginationResponseMeta + NextPage *PaginationMeta +} + +// List metrics using the provided options. +// +// Librato API docs: https://www.librato.com/docs/api/#retrieve-metrics +func (m *MetricsService) List(opts *ListMetricsOptions) ([]Metric, *ListMetricsResponse, error) { + u, err := urlWithOptions("metrics", opts) + if err != nil { + return nil, nil, err + } + + req, err := m.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var metricsResponse struct { + Query PaginationResponseMeta + Metrics []Metric + } + + _, err = m.client.Do(req, &metricsResponse) + if err != nil { + return nil, nil, err + } + + return metricsResponse.Metrics, + &ListMetricsResponse{ + ThisPage: &metricsResponse.Query, + NextPage: metricsResponse.Query.nextPage(opts.PaginationMeta), + }, + nil +} + +// Get a metric by name +// +// Librato API docs: https://www.librato.com/docs/api/#retrieve-metric-by-name +func (m *MetricsService) Get(name string) (*Metric, *http.Response, error) { + u := fmt.Sprintf("metrics/%s", name) + req, err := m.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + metric := new(Metric) + resp, err := m.client.Do(req, metric) + if err != nil { + return nil, resp, err + } + + return metric, resp, err +} + +type MeasurementSubmission struct { + MeasureTime *uint `json:"measure_time,omitempty"` + Source *string `json:"source,omitempty"` + Gauges []*GaugeMeasurement `json:"gauges,omitempty"` + Counters []*Measurement `json:"counters,omitempty"` +} + +type Measurement struct { + Name string `json:"name"` + Value *float64 `json:"value,omitempty"` + MeasureTime *uint `json:"measure_time,omitempty"` + Source *string `json:"source,omitempty"` +} + +type GaugeMeasurement struct { + *Measurement + Count *uint `json:"count,omitempty"` + Sum *float64 `json:"sum,omitempty"` + Max *float64 `json:"max,omitempty"` + Min *float64 `json:"min,omitempty"` + SumSquares *float64 `json:"sum_squares,omitempty"` +} + +// Submit metrics +// +// Librato API docs: https://www.librato.com/docs/api/#submit-metrics +func (m *MetricsService) Submit(measurements *MeasurementSubmission) (*http.Response, error) { + req, err := m.client.NewRequest("POST", "/metrics", measurements) + if err != nil { + return nil, err + } + + return m.client.Do(req, nil) +} + +// Edit a metric. +// +// Librato API docs: https://www.librato.com/docs/api/#update-metric-by-name +func (m *MetricsService) Edit(metric *Metric) (*http.Response, error) { + u := fmt.Sprintf("metrics/%s", *metric.Name) + + req, err := m.client.NewRequest("PUT", u, metric) + if err != nil { + return nil, err + } + + return m.client.Do(req, nil) +} + +// Delete a metric. +// +// Librato API docs: https://www.librato.com/docs/api/#delete-metric-by-name +func (m *MetricsService) Delete(name string) (*http.Response, error) { + u := fmt.Sprintf("metrics/%s", name) + req, err := m.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + return m.client.Do(req, nil) +} diff --git a/vendor/github.com/henrikhodne/go-librato/librato/pagination.go b/vendor/github.com/henrikhodne/go-librato/librato/pagination.go new file mode 100644 index 000000000..5b6934eeb --- /dev/null +++ b/vendor/github.com/henrikhodne/go-librato/librato/pagination.go @@ -0,0 +1,70 @@ +package librato + +import ( + "fmt" + "net/url" +) + +// PaginationResponseMeta contains pagination metadata from Librato API +// responses. +type PaginationResponseMeta struct { + Offset uint `json:"offset"` + Length uint `json:"length"` + Total uint `json:"total"` + Found uint `json:"found"` +} + +// Calculate the pagination metadata for the next page of the result set. +// Takes the metadata used to request the current page so that it can use the +// same sort/orderby options +func (p *PaginationResponseMeta) nextPage(originalQuery *PaginationMeta) (next *PaginationMeta) { + nextOffset := p.Offset + p.Length + + if nextOffset >= p.Found { + return nil + } + + next = &PaginationMeta{} + next.Offset = nextOffset + next.Length = p.Length + + if originalQuery != nil { + next.OrderBy = originalQuery.OrderBy + next.Sort = originalQuery.Sort + } + + return next +} + +// PaginationMeta contains metadata that the Librato API requires for pagination +// http://dev.librato.com/v1/pagination +type PaginationMeta struct { + Offset uint `url:"offset,omitempty"` + Length uint `url:"length,omitempty"` + OrderBy string `url:"orderby,omitempty"` + Sort string `url:"sort,omitempty"` +} + +// EncodeValues is implemented to allow other strucs to embed PaginationMeta and +// still use github.com/google/go-querystring/query to encode the struct. It +// makes PaginationMeta implement query.Encoder. +func (m *PaginationMeta) EncodeValues(name string, values *url.Values) error { + if m == nil { + return nil + } + + if m.Offset != 0 { + values.Set("offset", fmt.Sprintf("%d", m.Offset)) + } + if m.Length != 0 { + values.Set("length", fmt.Sprintf("%d", m.Length)) + } + if m.OrderBy != "" { + values.Set("orderby", m.OrderBy) + } + if m.Sort != "" { + values.Set("sort", m.Sort) + } + + return nil +} diff --git a/vendor/github.com/henrikhodne/go-librato/librato/services.go b/vendor/github.com/henrikhodne/go-librato/librato/services.go new file mode 100644 index 000000000..6d15d1e21 --- /dev/null +++ b/vendor/github.com/henrikhodne/go-librato/librato/services.go @@ -0,0 +1,90 @@ +package librato + +import ( + "fmt" + "net/http" +) + +// ServicesService handles communication with the Librato API methods related to +// notification services. +type ServicesService struct { + client *Client +} + +// Service represents a Librato Service. +type Service struct { + ID *uint `json:"id,omitempty"` + Type *string `json:"type,omitempty"` + Title *string `json:"title,omitempty"` + // This is an interface{} because it's a hash of settings + // specific to each service. + Settings map[string]string `json:"settings,omitempty"` +} + +func (a Service) String() string { + return Stringify(a) +} + +// Get a service by ID +// +// Librato API docs: https://www.librato.com/docs/api/#retrieve-specific-service +func (s *ServicesService) Get(id uint) (*Service, *http.Response, error) { + urlStr := fmt.Sprintf("services/%d", id) + + req, err := s.client.NewRequest("GET", urlStr, nil) + if err != nil { + return nil, nil, err + } + + service := new(Service) + resp, err := s.client.Do(req, service) + if err != nil { + return nil, resp, err + } + + return service, resp, err +} + +// Create a service +// +// Librato API docs: https://www.librato.com/docs/api/#create-a-service +func (s *ServicesService) Create(service *Service) (*Service, *http.Response, error) { + req, err := s.client.NewRequest("POST", "services", service) + if err != nil { + return nil, nil, err + } + + sv := new(Service) + resp, err := s.client.Do(req, sv) + if err != nil { + return nil, resp, err + } + + return sv, resp, err +} + +// Edit a service. +// +// Librato API docs: https://www.librato.com/docs/api/#update-a-service +func (s *ServicesService) Edit(serviceID uint, service *Service) (*http.Response, error) { + u := fmt.Sprintf("services/%d", serviceID) + req, err := s.client.NewRequest("PUT", u, service) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// Delete a service +// +// Librato API docs: https://www.librato.com/docs/api/#delete-a-service +func (s *ServicesService) Delete(id uint) (*http.Response, error) { + u := fmt.Sprintf("services/%d", id) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index ee31a419f..2850550dd 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1215,8 +1215,11 @@ "revision": "df949784da9ed028ee76df44652e42d37a09d7e4" }, { + "checksumSHA1": "jq2E42bB0kwKaerHXwJslUea4eM=", + "origin": "github.com/hashicorp/terraform/vendor/github.com/henrikhodne/go-librato/librato", "path": "github.com/henrikhodne/go-librato/librato", - "revision": "613abdebf4922c4d9d46bcb4bcf14ee18c08d7de" + "revision": "6e9aa4b1a8a8b735ad14b4f1c9542ef183e82dc2", + "revisionTime": "2016-08-11T07:26:26Z" }, { "comment": "v0.0.2-37-g5cd82f0", From cb6607953854c06f369801aa326209a35842c3a9 Mon Sep 17 00:00:00 2001 From: Anthony Stanton Date: Fri, 5 Aug 2016 23:16:32 +0200 Subject: [PATCH 2/9] Support for Librato Alerts and Services --- builtin/providers/librato/provider.go | 2 + .../librato/resource_librato_alert.go | 446 ++++++++++++++++++ .../librato/resource_librato_alert_test.go | 191 ++++++++ .../librato/resource_librato_service.go | 229 +++++++++ .../librato/resource_librato_service_test.go | 153 ++++++ .../providers/librato/r/alert.html.markdown | 66 +++ .../providers/librato/r/service.html.markdown | 44 ++ 7 files changed, 1131 insertions(+) create mode 100644 builtin/providers/librato/resource_librato_alert.go create mode 100644 builtin/providers/librato/resource_librato_alert_test.go create mode 100644 builtin/providers/librato/resource_librato_service.go create mode 100644 builtin/providers/librato/resource_librato_service_test.go create mode 100644 website/source/docs/providers/librato/r/alert.html.markdown create mode 100644 website/source/docs/providers/librato/r/service.html.markdown diff --git a/builtin/providers/librato/provider.go b/builtin/providers/librato/provider.go index 0b7894f6f..7b1a64016 100644 --- a/builtin/providers/librato/provider.go +++ b/builtin/providers/librato/provider.go @@ -28,6 +28,8 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "librato_space": resourceLibratoSpace(), "librato_space_chart": resourceLibratoSpaceChart(), + "librato_alert": resourceLibratoAlert(), + "librato_service": resourceLibratoService(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/librato/resource_librato_alert.go b/builtin/providers/librato/resource_librato_alert.go new file mode 100644 index 000000000..2d08be194 --- /dev/null +++ b/builtin/providers/librato/resource_librato_alert.go @@ -0,0 +1,446 @@ +package librato + +import ( + "bytes" + "fmt" + "log" + "math" + "strconv" + "time" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/henrikhodne/go-librato/librato" +) + +func resourceLibratoAlert() *schema.Resource { + return &schema.Resource{ + Create: resourceLibratoAlertCreate, + Read: resourceLibratoAlertRead, + Update: resourceLibratoAlertUpdate, + Delete: resourceLibratoAlertDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "id": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "active": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "rearm_seconds": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 600, + }, + "services": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "condition": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "metric_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "source": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "detect_reset": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "duration": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "threshold": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + }, + "summary_function": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + Set: resourceLibratoAlertConditionsHash, + }, + "attributes": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "runbook_url": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + // TODO add missing condition attrs + }, + } +} + +func resourceLibratoAlertConditionsHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["type"].(string))) + buf.WriteString(fmt.Sprintf("%f-", m["threshold"].(float64))) + buf.WriteString(fmt.Sprintf("%s-", m["metric_name"].(string))) + + return hashcode.String(buf.String()) +} + +func resourceLibratoAlertCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*librato.Client) + + alert := new(librato.Alert) + if v, ok := d.GetOk("name"); ok { + alert.Name = librato.String(v.(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"].(uint); ok { + condition.Duration = librato.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 Librato alert %s: %s", *alert.Name, err) + } + + 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 + }) + + return resourceLibratoAlertReadResult(d, alertResult) +} + +func resourceLibratoAlertRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*librato.Client) + id, err := strconv.ParseUint(d.Id(), 10, 0) + if err != nil { + return err + } + + 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 Librato Alert %s: %s", d.Id(), err) + } + + return resourceLibratoAlertReadResult(d, alert) +} + +func resourceLibratoAlertReadResult(d *schema.ResourceData, alert *librato.Alert) error { + d.SetId(strconv.FormatUint(uint64(*alert.ID), 10)) + if alert.ID != nil { + if err := d.Set("id", *alert.ID); err != nil { + return err + } + } + if alert.Name != nil { + if err := d.Set("name", *alert.Name); err != nil { + return err + } + } + 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 + } + } + if alert.Services != nil { + services := resourceLibratoAlertServicesGather(d, alert.Services.([]interface{})) + if err := d.Set("services", services); err != nil { + return err + } + } + if alert.Conditions != nil { + conditions := resourceLibratoAlertConditionsGather(d, alert.Conditions) + if err := d.Set("condition", conditions); err != nil { + return err + } + } + if alert.Attributes != nil { + attributes := resourceLibratoAlertAttributesGather(d, alert.Attributes) + if err := d.Set("attributes", attributes); err != nil { + return err + } + } + + return nil +} + +func resourceLibratoAlertServicesGather(d *schema.ResourceData, services []interface{}) []string { + retServices := make([]string, 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 resourceLibratoAlertConditionsGather(d *schema.ResourceData, conditions []librato.AlertCondition) []map[string]interface{} { + retConditions := make([]map[string]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"] = *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 resourceLibratoAlertAttributesGather(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 resourceLibratoAlertUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*librato.Client) + + alertID, err := strconv.ParseUint(d.Id(), 10, 0) + if err != nil { + return err + } + + alert := new(librato.Alert) + if d.HasChange("name") { + 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(d.Get("rearm_seconds").(uint)) + } + 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 + } + if d.HasChange("condition") { + 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"].(uint); ok { + condition.Duration = librato.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 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 + } + } + + _, err = client.Alerts.Edit(uint(alertID), alert) + if err != nil { + return fmt.Errorf("Error updating Librato alert: %s", err) + } + + return resourceLibratoAlertRead(d, meta) +} + +func resourceLibratoAlertDelete(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) + } + + 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")) + }) + + d.SetId("") + return nil +} diff --git a/builtin/providers/librato/resource_librato_alert_test.go b/builtin/providers/librato/resource_librato_alert_test.go new file mode 100644 index 000000000..0acf3a646 --- /dev/null +++ b/builtin/providers/librato/resource_librato_alert_test.go @@ -0,0 +1,191 @@ +package librato + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/henrikhodne/go-librato/librato" +) + +func TestAccLibratoAlert_Basic(t *testing.T) { + var alert librato.Alert + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibratoAlertDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckLibratoAlertConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLibratoAlertExists("librato_alert.foobar", &alert), + testAccCheckLibratoAlertName(&alert, "FooBar"), + resource.TestCheckResourceAttr( + "librato_alert.foobar", "name", "FooBar"), + ), + }, + }, + }) +} + +func TestAccLibratoAlert_Full(t *testing.T) { + var alert librato.Alert + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibratoAlertDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckLibratoAlertConfig_full, + Check: resource.ComposeTestCheckFunc( + testAccCheckLibratoAlertExists("librato_alert.foobar", &alert), + testAccCheckLibratoAlertName(&alert, "FooBar"), + resource.TestCheckResourceAttr( + "librato_alert.foobar", "name", "FooBar"), + ), + }, + }, + }) +} + +func TestAccLibratoAlert_Updated(t *testing.T) { + var alert librato.Alert + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibratoAlertDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckLibratoAlertConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLibratoAlertExists("librato_alert.foobar", &alert), + testAccCheckLibratoAlertName(&alert, "FooBar"), + resource.TestCheckResourceAttr( + "librato_alert.foobar", "name", "FooBar"), + ), + }, + resource.TestStep{ + Config: testAccCheckLibratoAlertConfig_new_value, + Check: resource.ComposeTestCheckFunc( + testAccCheckLibratoAlertExists("librato_alert.foobar", &alert), + testAccCheckLibratoAlertName(&alert, "BarBaz"), + resource.TestCheckResourceAttr( + "librato_alert.foobar", "name", "BarBaz"), + ), + }, + }, + }) +} + +func testAccCheckLibratoAlertDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*librato.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "librato_alert" { + continue + } + + id, err := strconv.ParseUint(rs.Primary.ID, 10, 0) + if err != nil { + return fmt.Errorf("ID not a number") + } + + _, _, err = client.Alerts.Get(uint(id)) + + if err == nil { + return fmt.Errorf("Alert still exists") + } + } + + return nil +} + +func testAccCheckLibratoAlertName(alert *librato.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) + } + + return nil + } +} + +func testAccCheckLibratoAlertExists(n string, alert *librato.Alert) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Alert ID is set") + } + + client := testAccProvider.Meta().(*librato.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)) + + if err != nil { + return err + } + + if foundAlert.ID == nil || *foundAlert.ID != uint(id) { + return fmt.Errorf("Alert not found") + } + + *alert = *foundAlert + + return nil + } +} + +const testAccCheckLibratoAlertConfig_basic = ` +resource "librato_alert" "foobar" { + name = "FooBar" + description = "A Test Alert" +}` + +const testAccCheckLibratoAlertConfig_new_value = ` +resource "librato_alert" "foobar" { + name = "BarBaz" + description = "A Test Alert" +}` + +const testAccCheckLibratoAlertConfig_full = ` +resource "librato_service" "foobar" { + title = "Foo Bar" + type = "mail" + settings = < Date: Wed, 31 Aug 2016 14:10:37 +0200 Subject: [PATCH 3/9] fixup! Support for Librato Alerts and Services --- builtin/providers/librato/resource_librato_alert.go | 1 - builtin/providers/librato/resource_librato_service.go | 6 ------ 2 files changed, 7 deletions(-) diff --git a/builtin/providers/librato/resource_librato_alert.go b/builtin/providers/librato/resource_librato_alert.go index 2d08be194..0ee551730 100644 --- a/builtin/providers/librato/resource_librato_alert.go +++ b/builtin/providers/librato/resource_librato_alert.go @@ -100,7 +100,6 @@ func resourceLibratoAlert() *schema.Resource { }, }, }, - // TODO add missing condition attrs }, } } diff --git a/builtin/providers/librato/resource_librato_service.go b/builtin/providers/librato/resource_librato_service.go index 9db8fc450..c012b6307 100644 --- a/builtin/providers/librato/resource_librato_service.go +++ b/builtin/providers/librato/resource_librato_service.go @@ -1,8 +1,6 @@ package librato import ( - // "crypto/sha1" - // "encoding/hex" "encoding/json" "fmt" "log" @@ -39,10 +37,6 @@ func resourceLibratoService() *schema.Resource { Type: schema.TypeString, Required: true, StateFunc: normalizeJson, - //StateFunc: func(v interface{}) string { - // hash := sha1.Sum([]byte(v.(string))) - // return hex.EncodeToString(hash[:]) - //}, }, }, } From 783b2e5780d752103e18f84797811039d462ea9f Mon Sep 17 00:00:00 2001 From: Anthony Stanton Date: Wed, 31 Aug 2016 17:19:53 +0200 Subject: [PATCH 4/9] fixup! Support for Librato Alerts and Services --- builtin/providers/librato/resource_librato_alert.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/builtin/providers/librato/resource_librato_alert.go b/builtin/providers/librato/resource_librato_alert.go index 0ee551730..7f778b50b 100644 --- a/builtin/providers/librato/resource_librato_alert.go +++ b/builtin/providers/librato/resource_librato_alert.go @@ -108,7 +108,10 @@ func resourceLibratoAlertConditionsHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) buf.WriteString(fmt.Sprintf("%s-", m["type"].(string))) - buf.WriteString(fmt.Sprintf("%f-", m["threshold"].(float64))) + threshold, present := m["threshold"] + if present { + buf.WriteString(fmt.Sprintf("%f-", threshold.(float64))) + } buf.WriteString(fmt.Sprintf("%s-", m["metric_name"].(string))) return hashcode.String(buf.String()) From 4b2e11ac6386774dea1267e1c3ff41f19334adde Mon Sep 17 00:00:00 2001 From: Anthony Stanton Date: Wed, 31 Aug 2016 17:20:08 +0200 Subject: [PATCH 5/9] fixup! Support for Librato Alerts and Services --- .../librato/resource_librato_alert.go | 57 +++++-------------- .../librato/resource_librato_service.go | 25 ++------ 2 files changed, 18 insertions(+), 64 deletions(-) diff --git a/builtin/providers/librato/resource_librato_alert.go b/builtin/providers/librato/resource_librato_alert.go index 7f778b50b..e8364d84c 100644 --- a/builtin/providers/librato/resource_librato_alert.go +++ b/builtin/providers/librato/resource_librato_alert.go @@ -229,49 +229,20 @@ func resourceLibratoAlertRead(d *schema.ResourceData, meta interface{}) error { func resourceLibratoAlertReadResult(d *schema.ResourceData, alert *librato.Alert) error { d.SetId(strconv.FormatUint(uint64(*alert.ID), 10)) - if alert.ID != nil { - if err := d.Set("id", *alert.ID); err != nil { - return err - } - } - if alert.Name != nil { - if err := d.Set("name", *alert.Name); err != nil { - return err - } - } - 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 - } - } - if alert.Services != nil { - services := resourceLibratoAlertServicesGather(d, alert.Services.([]interface{})) - if err := d.Set("services", services); err != nil { - return err - } - } - if alert.Conditions != nil { - conditions := resourceLibratoAlertConditionsGather(d, alert.Conditions) - if err := d.Set("condition", conditions); err != nil { - return err - } - } - if alert.Attributes != nil { - attributes := resourceLibratoAlertAttributesGather(d, alert.Attributes) - if err := d.Set("attributes", attributes); err != nil { - return err - } - } + d.Set("id", *alert.ID) + d.Set("name", *alert.Name) + d.Set("description", *alert.Description) + d.Set("active", *alert.Active) + d.Set("rearm_seconds", *alert.RearmSeconds) + + services := resourceLibratoAlertServicesGather(d, alert.Services.([]interface{})) + d.Set("services", services) + + conditions := resourceLibratoAlertConditionsGather(d, alert.Conditions) + d.Set("condition", conditions) + + attributes := resourceLibratoAlertAttributesGather(d, alert.Attributes) + d.Set("attributes", attributes) return nil } diff --git a/builtin/providers/librato/resource_librato_service.go b/builtin/providers/librato/resource_librato_service.go index c012b6307..655667667 100644 --- a/builtin/providers/librato/resource_librato_service.go +++ b/builtin/providers/librato/resource_librato_service.go @@ -138,27 +138,10 @@ func resourceLibratoServiceRead(d *schema.ResourceData, meta interface{}) error func resourceLibratoServiceReadResult(d *schema.ResourceData, service *librato.Service) error { d.SetId(strconv.FormatUint(uint64(*service.ID), 10)) - if service.ID != nil { - if err := d.Set("id", *service.ID); err != nil { - return err - } - } - if service.Type != nil { - if err := d.Set("type", *service.Type); err != nil { - return err - } - } - if service.Title != nil { - if err := d.Set("title", *service.Title); err != nil { - return err - } - } - if service.Settings != nil { - settings, _ := resourceLibratoServicesFlatten(service.Settings) - if err := d.Set("settings", settings); err != nil { - return err - } - } + d.Set("id", *service.ID) + d.Set("type", *service.Type) + d.Set("title", *service.Title) + d.Set("settings", settings) return nil } From 83d4a714236e12f2f9993e3065a8055eb6fd3114 Mon Sep 17 00:00:00 2001 From: Anthony Stanton Date: Wed, 31 Aug 2016 17:20:20 +0200 Subject: [PATCH 6/9] fixup! Support for Librato Alerts and Services --- website/source/layouts/librato.erb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/source/layouts/librato.erb b/website/source/layouts/librato.erb index cbd6f718f..242aedf13 100644 --- a/website/source/layouts/librato.erb +++ b/website/source/layouts/librato.erb @@ -13,6 +13,12 @@ > Resources