terraform/builtin/providers/datadog/resource_datadog_timeboard.go

727 lines
20 KiB
Go

package datadog
import (
"encoding/json"
"fmt"
"log"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/zorkian/go-datadog-api"
)
func resourceDatadogTimeboard() *schema.Resource {
request := &schema.Schema{
Type: schema.TypeList,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"q": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"stacked": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "line",
},
"aggregator": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateAggregatorMethod,
},
"style": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
"conditional_format": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Description: "A list of conditional formatting rules.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"palette": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The palette to use if this condition is met.",
},
"comparator": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Comparator (<, >, etc)",
},
"custom_bg_color": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Custom background color (e.g., #205081)",
},
"value": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Value that is threshold for conditional format",
},
"custom_fg_color": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Custom foreground color (e.g., #59afe1)",
},
},
},
},
"change_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Type of change for change graphs.",
},
"order_direction": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Sort change graph in ascending or descending order.",
},
"compare_to": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The time period to compare change against in change graphs.",
},
"increase_good": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "Decides whether to represent increases as good or bad in change graphs.",
},
"order_by": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The field a change graph will be ordered by.",
},
"extra_col": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "If set to 'present', this will include the present values in change graphs.",
},
},
},
}
marker := &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"value": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"label": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
}
graph := &schema.Schema{
Type: schema.TypeList,
Required: true,
Description: "A list of graph definitions.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"title": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The name of the graph.",
},
"events": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Description: "Filter for events to be overlayed on the graph.",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"viz": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"request": request,
"marker": marker,
"yaxis": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
"autoscale": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "Automatically scale graphs",
},
"text_align": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "How to align text",
},
"precision": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "How many digits to show",
},
"custom_unit": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Use a custom unit (like 'users')",
},
"style": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
"group": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Description: "A list of groupings for hostmap type graphs.",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"include_no_metric_hosts": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "Include hosts without metrics in hostmap graphs",
},
"scope": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Description: "A list of scope filters for hostmap type graphs.",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"include_ungrouped_hosts": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "Include ungrouped hosts in hostmap graphs",
},
},
},
}
template_variable := &schema.Schema{
Type: schema.TypeList,
Optional: true,
Description: "A list of template variables for using Dashboard templating.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The name of the variable.",
},
"prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The tag prefix associated with the variable. Only tags with this prefix will appear in the variable dropdown.",
},
"default": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The default value for the template variable on dashboard load.",
},
},
},
}
return &schema.Resource{
Create: resourceDatadogTimeboardCreate,
Update: resourceDatadogTimeboardUpdate,
Read: resourceDatadogTimeboardRead,
Delete: resourceDatadogTimeboardDelete,
Exists: resourceDatadogTimeboardExists,
Importer: &schema.ResourceImporter{
State: resourceDatadogTimeboardImport,
},
Schema: map[string]*schema.Schema{
"title": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The name of the dashboard.",
},
"description": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "A description of the dashboard's content.",
},
"read_only": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"graph": graph,
"template_variable": template_variable,
},
}
}
func appendConditionalFormats(datadogRequest *datadog.GraphDefinitionRequest, terraformFormats *[]interface{}) {
for _, t_ := range *terraformFormats {
t := t_.(map[string]interface{})
d := datadog.DashboardConditionalFormat{
Comparator: t["comparator"].(string),
}
if palette, ok := t["palette"]; ok {
d.Palette = palette.(string)
}
if customBgColor, ok := t["custom_bg_color"]; ok {
d.CustomBgColor = customBgColor.(string)
}
if customFgColor, ok := t["custom_fg_color"]; ok {
d.CustomFgColor = customFgColor.(string)
}
if value, ok := t["value"]; ok {
d.Value = json.Number(value.(string))
}
datadogRequest.ConditionalFormats = append(datadogRequest.ConditionalFormats, d)
}
}
func buildTemplateVariables(terraformTemplateVariables *[]interface{}) *[]datadog.TemplateVariable {
datadogTemplateVariables := make([]datadog.TemplateVariable, len(*terraformTemplateVariables))
for i, t_ := range *terraformTemplateVariables {
t := t_.(map[string]interface{})
datadogTemplateVariables[i] = datadog.TemplateVariable{
Name: t["name"].(string),
Prefix: t["prefix"].(string),
Default: t["default"].(string)}
}
return &datadogTemplateVariables
}
func appendRequests(datadogGraph *datadog.Graph, terraformRequests *[]interface{}) {
for _, t_ := range *terraformRequests {
t := t_.(map[string]interface{})
d := datadog.GraphDefinitionRequest{
Query: t["q"].(string),
Type: t["type"].(string),
Aggregator: t["aggregator"].(string),
}
if stacked, ok := t["stacked"]; ok {
d.Stacked = stacked.(bool)
}
if style, ok := t["style"]; ok {
s, _ := style.(map[string]interface{})
style := struct {
Palette *string `json:"palette,omitempty"`
Width *string `json:"width,omitempty"`
Type *string `json:"type,omitempty"`
}{}
if palette_, ok := s["palette"]; ok {
palette := palette_.(string)
style.Palette = &palette
}
if width, ok := s["width"]; ok {
width := width.(string)
style.Width = &width
}
if type_, ok := s["type"]; ok {
style_type := type_.(string)
style.Type = &style_type
}
d.Style = &style
}
if changeType, ok := t["change_type"]; ok {
d.ChangeType = changeType.(string)
}
if compareTo, ok := t["compare_to"]; ok {
d.CompareTo = compareTo.(string)
}
if increaseGood, ok := t["increase_good"]; ok {
d.IncreaseGood = increaseGood.(bool)
}
if orderBy, ok := t["order_by"]; ok {
d.OrderBy = orderBy.(string)
}
if extraCol, ok := t["extra_col"]; ok {
d.ExtraCol = extraCol.(string)
}
if orderDirection, ok := t["order_direction"]; ok {
d.OrderDirection = orderDirection.(string)
}
if terraformConditionalFormats, ok := t["conditional_format"]; ok {
formats := terraformConditionalFormats.([]interface{})
appendConditionalFormats(&d, &formats)
}
datadogGraph.Definition.Requests = append(datadogGraph.Definition.Requests, d)
}
}
func appendEvents(datadogGraph *datadog.Graph, terraformEvents *[]interface{}) {
for _, t_ := range *terraformEvents {
d := struct {
Query string `json:"q"`
}{
t_.(string),
}
datadogGraph.Definition.Events = append(datadogGraph.Definition.Events, d)
}
}
func appendMarkers(datadogGraph *datadog.Graph, terraformMarkers *[]interface{}) {
for _, t_ := range *terraformMarkers {
t := t_.(map[string]interface{})
d := datadog.GraphDefinitionMarker{
Type: t["type"].(string),
Value: t["value"].(string),
}
if label, ok := t["label"]; ok {
d.Label = label.(string)
}
datadogGraph.Definition.Markers = append(datadogGraph.Definition.Markers, d)
}
}
func buildGraphs(terraformGraphs *[]interface{}) *[]datadog.Graph {
datadogGraphs := make([]datadog.Graph, len(*terraformGraphs))
for i, t_ := range *terraformGraphs {
t := t_.(map[string]interface{})
datadogGraphs[i] = datadog.Graph{Title: t["title"].(string)}
d := &datadogGraphs[i]
d.Definition.Viz = t["viz"].(string)
if yaxis_, ok := t["yaxis"]; ok {
yaxis := yaxis_.(map[string]interface{})
if min_, ok := yaxis["min"]; ok {
min, _ := strconv.ParseFloat(min_.(string), 64)
d.Definition.Yaxis.Min = &min
}
if max_, ok := yaxis["max"]; ok {
max, _ := strconv.ParseFloat(max_.(string), 64)
d.Definition.Yaxis.Max = &max
}
if scale_, ok := yaxis["scale"]; ok {
scale := scale_.(string)
d.Definition.Yaxis.Scale = &scale
}
}
if autoscale, ok := t["autoscale"]; ok {
d.Definition.Autoscale = autoscale.(bool)
}
if textAlign, ok := t["text_align"]; ok {
d.Definition.TextAlign = textAlign.(string)
}
if precision, ok := t["precision"]; ok {
d.Definition.Precision = precision.(string)
}
if customUnit, ok := t["custom_unit"]; ok {
d.Definition.CustomUnit = customUnit.(string)
}
if style, ok := t["style"]; ok {
s := style.(map[string]interface{})
style := struct {
Palette *string `json:"palette,omitempty"`
PaletteFlip *bool `json:"paletteFlip,omitempty"`
}{}
if palette_, ok := s["palette"]; ok {
palette := palette_.(string)
style.Palette = &palette
}
if paletteFlip_, ok := s["palette_flip"]; ok {
paletteFlip, _ := strconv.ParseBool(paletteFlip_.(string))
style.PaletteFlip = &paletteFlip
}
d.Definition.Style = &style
}
if groups, ok := t["group"]; ok {
for _, g := range groups.(*schema.Set).List() {
d.Definition.Groups = append(d.Definition.Groups, g.(string))
}
}
if includeNoMetricHosts, ok := t["include_no_metric_hosts"]; ok {
d.Definition.IncludeNoMetricHosts = includeNoMetricHosts.(bool)
}
if scopes, ok := t["scope"]; ok {
for _, s := range scopes.(*schema.Set).List() {
d.Definition.Scopes = append(d.Definition.Groups, s.(string))
}
}
if includeUngroupedHosts, ok := t["include_ungrouped_hosts"]; ok {
d.Definition.IncludeUngroupedHosts = includeUngroupedHosts.(bool)
}
terraformMarkers := t["marker"].([]interface{})
appendMarkers(d, &terraformMarkers)
terraformEvents := t["events"].(*schema.Set).List()
appendEvents(d, &terraformEvents)
terraformRequests := t["request"].([]interface{})
appendRequests(d, &terraformRequests)
}
return &datadogGraphs
}
func buildTimeboard(d *schema.ResourceData) (*datadog.Dashboard, error) {
var id int
if d.Id() != "" {
var err error
id, err = strconv.Atoi(d.Id())
if err != nil {
return nil, err
}
}
terraformGraphs := d.Get("graph").([]interface{})
terraformTemplateVariables := d.Get("template_variable").([]interface{})
return &datadog.Dashboard{
Id: id,
Title: d.Get("title").(string),
Description: d.Get("description").(string),
ReadOnly: d.Get("read_only").(bool),
Graphs: *buildGraphs(&terraformGraphs),
TemplateVariables: *buildTemplateVariables(&terraformTemplateVariables),
}, nil
}
func resourceDatadogTimeboardCreate(d *schema.ResourceData, meta interface{}) error {
timeboard, err := buildTimeboard(d)
if err != nil {
return fmt.Errorf("Failed to parse resource configuration: %s", err.Error())
}
timeboard, err = meta.(*datadog.Client).CreateDashboard(timeboard)
if err != nil {
return fmt.Errorf("Failed to create timeboard using Datadog API: %s", err.Error())
}
d.SetId(strconv.Itoa(timeboard.Id))
return nil
}
func resourceDatadogTimeboardUpdate(d *schema.ResourceData, meta interface{}) error {
timeboard, err := buildTimeboard(d)
if err != nil {
return fmt.Errorf("Failed to parse resource configuration: %s", err.Error())
}
if err = meta.(*datadog.Client).UpdateDashboard(timeboard); err != nil {
return fmt.Errorf("Failed to update timeboard using Datadog API: %s", err.Error())
}
return resourceDatadogTimeboardRead(d, meta)
}
func appendTerraformGraphRequests(datadogRequests []datadog.GraphDefinitionRequest, requests *[]map[string]interface{}) {
for _, datadogRequest := range datadogRequests {
request := map[string]interface{}{}
request["q"] = datadogRequest.Query
request["stacked"] = datadogRequest.Stacked
request["type"] = datadogRequest.Type
if datadogRequest.Style != nil {
style := map[string]string{}
if datadogRequest.Style.Palette != nil {
style["palette"] = *datadogRequest.Style.Palette
}
if datadogRequest.Style.Type != nil {
style["type"] = *datadogRequest.Style.Type
}
if datadogRequest.Style.Width != nil {
style["width"] = *datadogRequest.Style.Width
}
request["style"] = style
}
conditionalFormats := []map[string]interface{}{}
for _, cf := range datadogRequest.ConditionalFormats {
conditionalFormat := map[string]interface{}{
"palette": cf.Palette,
"comparator": cf.Comparator,
"custom_bg_color": cf.CustomBgColor,
"value": cf.Value,
"custom_fg_color": cf.CustomFgColor,
}
conditionalFormats = append(conditionalFormats, conditionalFormat)
}
request["conditional_format"] = conditionalFormats
request["change_type"] = datadogRequest.ChangeType
request["order_direction"] = datadogRequest.OrderDirection
request["compare_to"] = datadogRequest.CompareTo
request["increase_good"] = datadogRequest.IncreaseGood
request["order_by"] = datadogRequest.OrderBy
request["extra_col"] = datadogRequest.ExtraCol
*requests = append(*requests, request)
}
}
func buildTerraformGraph(datadog_graph datadog.Graph) map[string]interface{} {
graph := map[string]interface{}{}
graph["title"] = datadog_graph.Title
definition := datadog_graph.Definition
graph["viz"] = definition.Viz
events := []string{}
for _, datadog_event := range definition.Events {
events = append(events, datadog_event.Query)
}
graph["events"] = events
markers := []map[string]interface{}{}
for _, datadog_marker := range definition.Markers {
marker := map[string]interface{}{
"type": datadog_marker.Type,
"value": datadog_marker.Value,
"label": datadog_marker.Label,
}
markers = append(markers, marker)
}
graph["marker"] = markers
yaxis := map[string]string{}
if definition.Yaxis.Min != nil {
yaxis["min"] = strconv.FormatFloat(*definition.Yaxis.Min, 'f', -1, 64)
}
if definition.Yaxis.Max != nil {
yaxis["max"] = strconv.FormatFloat(*definition.Yaxis.Max, 'f', -1, 64)
}
if definition.Yaxis.Scale != nil {
yaxis["scale"] = *definition.Yaxis.Scale
}
graph["yaxis"] = yaxis
graph["autoscale"] = definition.Autoscale
graph["text_align"] = definition.TextAlign
graph["precision"] = definition.Precision
graph["custom_unit"] = definition.CustomUnit
if definition.Style != nil {
style := map[string]string{}
if definition.Style.Palette != nil {
style["palette"] = *definition.Style.Palette
}
if definition.Style.PaletteFlip != nil {
style["palette_flip"] = strconv.FormatBool(*definition.Style.PaletteFlip)
}
graph["style"] = style
}
graph["group"] = definition.Groups
graph["include_no_metric_hosts"] = definition.IncludeNoMetricHosts
graph["scope"] = definition.Scopes
graph["include_ungrouped_hosts"] = definition.IncludeUngroupedHosts
requests := []map[string]interface{}{}
appendTerraformGraphRequests(definition.Requests, &requests)
graph["request"] = requests
return graph
}
func resourceDatadogTimeboardRead(d *schema.ResourceData, meta interface{}) error {
id, err := strconv.Atoi(d.Id())
if err != nil {
return err
}
timeboard, err := meta.(*datadog.Client).GetDashboard(id)
if err != nil {
return err
}
log.Printf("[DEBUG] timeboard: %v", timeboard)
d.Set("title", timeboard.Title)
d.Set("description", timeboard.Description)
graphs := []map[string]interface{}{}
for _, datadog_graph := range timeboard.Graphs {
graphs = append(graphs, buildTerraformGraph(datadog_graph))
}
d.Set("graph", graphs)
templateVariables := []map[string]string{}
for _, templateVariable := range timeboard.TemplateVariables {
tv := map[string]string{
"name": templateVariable.Name,
"prefix": templateVariable.Prefix,
"default": templateVariable.Default,
}
templateVariables = append(templateVariables, tv)
}
d.Set("template_variable", templateVariables)
return nil
}
func resourceDatadogTimeboardDelete(d *schema.ResourceData, meta interface{}) error {
id, err := strconv.Atoi(d.Id())
if err != nil {
return err
}
if err = meta.(*datadog.Client).DeleteDashboard(id); err != nil {
return err
}
return nil
}
func resourceDatadogTimeboardImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
if err := resourceDatadogTimeboardRead(d, meta); err != nil {
return nil, err
}
return []*schema.ResourceData{d}, nil
}
func resourceDatadogTimeboardExists(d *schema.ResourceData, meta interface{}) (b bool, e error) {
id, err := strconv.Atoi(d.Id())
if err != nil {
return false, err
}
if _, err = meta.(*datadog.Client).GetDashboard(id); err != nil {
if strings.Contains(err.Error(), "404 Not Found") {
return false, nil
}
return false, err
}
return true, nil
}
func validateAggregatorMethod(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
validMethods := map[string]struct{}{
"average": {},
"max": {},
"min": {},
"sum": {},
}
if _, ok := validMethods[value]; !ok {
errors = append(errors, fmt.Errorf(
`%q contains an invalid method %q. Valid methods are either "average", "max", "min", or "sum"`, k, value))
}
return
}