Skip to content

Commit

Permalink
Allow adding metadata aliases for widgets in screenboards and timeboa…
Browse files Browse the repository at this point in the history
…rds. Fixes DataDog#61
  • Loading branch information
Slavek Kabrda committed May 23, 2019
1 parent 735a234 commit fd7c536
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 2 deletions.
44 changes: 43 additions & 1 deletion datadog/resource_datadog_screenboard.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package datadog

import (
"bytes"
"encoding/json"
"fmt"
"log"
Expand Down Expand Up @@ -154,6 +155,11 @@ func resourceDatadogScreenboard() *schema.Resource {
Type: schema.TypeBool,
Optional: true,
},
"metadata_json": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateMetadataJSON,
},
},
},
}
Expand Down Expand Up @@ -582,6 +588,25 @@ func resourceDatadogScreenboard() *schema.Resource {
// # Convenience functions to safely pass info from Terraform to the Datadog API wrapper #
// #######################################################################################

func getMetadataFromJSON(jsonBytes []byte, unmarshalled interface{}) error {
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
// make sure we return errors on attributes that we don't expect in metadata
decoder.DisallowUnknownFields()
err := decoder.Decode(unmarshalled)
if err != nil {
return fmt.Errorf("Failed to unmarshal metadata_json: %s", err)
}
return nil
}

func validateMetadataJSON(v interface{}, k string) (ws []string, errors []error) {
err := getMetadataFromJSON([]byte(v.(string)), &map[string]datadog.TileDefMetadata{})
if err != nil {
errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %s", k, err))
}
return
}

func setStringFromDict(dict map[string]interface{}, key string, field **string) {
if v, ok := dict[key]; ok && v != nil {
*field = datadog.String(v.(string))
Expand Down Expand Up @@ -642,6 +667,12 @@ func setStringListFromDict(dict map[string]interface{}, key string, field *[]*st
}
}

func setMetadataFromDict(dict map[string]interface{}, key string, field *map[string]datadog.TileDefMetadata) {
if v, ok := dict[key].(map[string]datadog.TileDefMetadata); ok {
*field = v
}
}

func setFromDict(dict map[string]interface{}, key string, field interface{}) {
switch field.(type) {
case **string:
Expand All @@ -654,6 +685,8 @@ func setFromDict(dict map[string]interface{}, key string, field interface{}) {
setJSONNumberFromDict(dict, key, field.(**json.Number))
case **datadog.PrecisionT:
setPrecisionTFromDict(dict, key, field.(**datadog.PrecisionT))
case *map[string]datadog.TileDefMetadata:
setMetadataFromDict(dict, key, field.(*map[string]datadog.TileDefMetadata))
case *[]*string:
setStringListFromDict(dict, key, field.(*[]*string))
default:
Expand Down Expand Up @@ -717,6 +750,9 @@ func buildTileDefRequests(source interface{}) []datadog.TileDefRequest {
r := []datadog.TileDefRequest{}
for _, request := range requests {
requestMap := request.(map[string]interface{})
metadata := map[string]datadog.TileDefMetadata{}
getMetadataFromJSON([]byte(requestMap["metadata_json"].(string)), &metadata)
requestMap["metadata"] = metadata
d := datadog.TileDefRequest{}
batchSetFromDict(batch{
dict: requestMap,
Expand All @@ -735,6 +771,7 @@ func buildTileDefRequests(source interface{}) []datadog.TileDefRequest {
{"extra_col", &d.ExtraCol},
{"increase_good", &d.IncreaseGood},
{"tag_filters", &d.TagFilters},
{"metadata", &d.Metadata},
}})

// request.style
Expand Down Expand Up @@ -1017,7 +1054,8 @@ func resourceDatadogScreenboardCreate(d *schema.ResourceData, meta interface{})
return fmt.Errorf("Failed to create screenboard using Datadog API: %s", err.Error())
}
d.SetId(strconv.Itoa(screenboard.GetId()))
return nil

return resourceDatadogScreenboardRead(d, meta)
}

// #######################################################################################
Expand Down Expand Up @@ -1156,6 +1194,10 @@ func buildTFTileDefRequests(d []datadog.TileDefRequest) []interface{} {
{"increase_good", ddRequest.IncreaseGood},
{"tag_filters", ddRequest.TagFilters},
}})
if ddRequest.Metadata != nil {
res, _ := json.Marshal(ddRequest.Metadata)
tfRequest["metadata_json"] = string(res)
}

// request.style
if ddRequest.Style != nil {
Expand Down
8 changes: 8 additions & 0 deletions datadog/resource_datadog_screenboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ resource "datadog_screenboard" "acceptance_test" {
type = "dashed"
width = "thin"
}
metadata_json = jsonencode({
"avg:system.cpu.user{*}" = {
"alias" = "Avg CPU user"
}
})
}
marker {
Expand Down Expand Up @@ -530,6 +536,8 @@ func TestAccDatadogScreenboard_update(t *testing.T) {
resource.TestCheckResourceAttr("datadog_screenboard.acceptance_test", "widget.1.tile_def.0.request.0.extra_col", ""),
resource.TestCheckResourceAttr("datadog_screenboard.acceptance_test", "widget.1.tile_def.0.request.0.increase_good", "false"),
resource.TestCheckResourceAttr("datadog_screenboard.acceptance_test", "widget.1.tile_def.0.request.0.limit", "0"),
resource.TestCheckResourceAttr("datadog_screenboard.acceptance_test", "widget.1.tile_def.0.request.0.metadata_json",
"{\"avg:system.cpu.user{*}\":{\"alias\":\"Avg CPU user\"}}"),
resource.TestCheckResourceAttr("datadog_screenboard.acceptance_test", "widget.1.tile_def.0.request.0.metric", ""),
resource.TestCheckResourceAttr("datadog_screenboard.acceptance_test", "widget.1.tile_def.0.request.0.order_by", ""),
resource.TestCheckResourceAttr("datadog_screenboard.acceptance_test", "widget.1.tile_def.0.request.0.order_dir", ""),
Expand Down
17 changes: 16 additions & 1 deletion datadog/resource_datadog_timeboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ func resourceDatadogTimeboard() *schema.Resource {
Type: schema.TypeMap,
Optional: true,
},
"metadata_json": {
Type: schema.TypeString,
Optional: true,
// NOTE: this is using functions from resource_datadog_screenboard
// since the metadata attribute is the same for both of these boards
ValidateFunc: validateMetadataJSON,
},
"conditional_format": {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -404,6 +411,10 @@ func appendRequests(datadogGraph *datadog.Graph, terraformRequests *[]interface{
_v := v.([]interface{})
appendConditionalFormats(&d, &_v)
}
if v, ok := t["metadata_json"]; ok {
d.Metadata = map[string]datadog.GraphDefinitionMetadata{}
getMetadataFromJSON([]byte(v.(string)), &d.Metadata)
}

datadogGraph.Definition.Requests = append(datadogGraph.Definition.Requests, d)
}
Expand Down Expand Up @@ -579,7 +590,7 @@ func resourceDatadogTimeboardCreate(d *schema.ResourceData, meta interface{}) er
return fmt.Errorf("Failed to create timeboard using Datadog API: %s", err.Error())
}
d.SetId(strconv.Itoa(timeboard.GetId()))
return nil
return resourceDatadogTimeboardRead(d, meta)
}

func resourceDatadogTimeboardUpdate(d *schema.ResourceData, meta interface{}) error {
Expand Down Expand Up @@ -654,6 +665,10 @@ func appendTerraformGraphRequests(datadogRequests []datadog.GraphDefinitionReque
if v, ok := datadogRequest.GetExtraColOk(); ok {
request["extra_col"] = v
}
if datadogRequest.Metadata != nil {
res, _ := json.Marshal(datadogRequest.Metadata)
request["metadata_json"] = string(res)
}

*requests = append(*requests, request)
}
Expand Down
7 changes: 7 additions & 0 deletions datadog/resource_datadog_timeboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ resource "datadog_timeboard" "acceptance_test" {
viz = "timeseries"
request {
q = "avg:redis.info.latency_ms{$host}"
metadata_json = jsonencode({
"avg:redis.info.latency_ms{$host}": {
"alias": "Redis latency"
}
})
}
}
graph {
Expand Down Expand Up @@ -156,6 +161,8 @@ func TestAccDatadogTimeboard_update(t *testing.T) {
resource.TestCheckResourceAttr("datadog_timeboard.acceptance_test", "graph.0.title", "Redis latency (ms)"),
resource.TestCheckResourceAttr("datadog_timeboard.acceptance_test", "graph.0.viz", "timeseries"),
resource.TestCheckResourceAttr("datadog_timeboard.acceptance_test", "graph.0.request.0.q", "avg:redis.info.latency_ms{$host}"),
resource.TestCheckResourceAttr("datadog_timeboard.acceptance_test", "graph.0.request.0.metadata_json",
"{\"avg:redis.info.latency_ms{$host}\":{\"alias\":\"Redis latency\"}}"),
resource.TestCheckResourceAttr("datadog_timeboard.acceptance_test", "graph.1.title", "Redis memory usage"),
resource.TestCheckResourceAttr("datadog_timeboard.acceptance_test", "graph.1.viz", "timeseries"),
resource.TestCheckResourceAttr("datadog_timeboard.acceptance_test", "graph.1.request.0.q", "avg:redis.mem.used{$host} - avg:redis.mem.lua{$host}, avg:redis.mem.lua{$host}"),
Expand Down
32 changes: 32 additions & 0 deletions website/docs/r/screenboard.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ resource "datadog_screenboard" "acceptance_test" {
type = "dashed"
width = "thin"
}
# NOTE: this will only work with TF >= 0.12; see metadata_json
# documentation below for example on usage with TF < 0.12
metadata_json = jsonencode({
"avg:system.cpu.user{*}": {
"alias": "CPU Usage"
}
})
}
marker {
Expand Down Expand Up @@ -547,6 +555,30 @@ Nested `widget` `tile_def` `request` blocks have the following structure:
- `increase_good` - (Optional, only for widgets of type "change") Boolean indicating whether an increase in the value is good (thus displayed in green) or not (thus displayed in red).
- `style` - (Optional, only for widgets of type "timeseries", "query_value", "toplist", "process") describing how to display the widget. The structure of this block is described below. At most one such block should be present in a given request block.
- `conditional_format` - (Optional) Nested block to customize the style if certain conditions are met. Currently only applies to `Query Value` and `Top List` type graphs.
* `metadata_json` - (Optional) A JSON blob (preferrably created using [jsonencode](https://www.terraform.io/docs/configuration/functions/jsonencode.html)) representing mapping of query expressions to alias names. Note that the query expressions in `metadata_json` will be ignored if they're not present in the query. For example, this is how you define `metadata_json` with Terraform >= 0.12:
```
metadata_json = jsonencode({
"avg:redis.info.latency_ms{$host}": {
"alias": "Redis latency"
}
})
```
And here's how you define `metadata_json` with Terraform < 0.12:
```
variable "my_metadata" {
default = {
"avg:redis.info.latency_ms{$host}" = {
"alias": "Redis latency"
}
}
}
resource "datadog_screenboard" "SomeScreenboard" {
...
metadata_json = "${jsonencode(var.my_metadata)}"
}
```
Note that this has to be a JSON blob because of [limitations](https://github.com/hashicorp/terraform/issues/6215) of Terraform's handling complex nested structures. This is also why the key is called `metadata_json` even though it sets `metadata` attribute on the API call.

### Nested `widget` `tile_def` `request` `style` block

Expand Down
32 changes: 32 additions & 0 deletions website/docs/r/timeboard.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ resource "datadog_timeboard" "redis" {
request {
q = "avg:redis.info.latency_ms{$host}"
type = "bars"
# NOTE: this will only work with TF >= 0.12; see metadata_json
# documentation below for example on usage with TF < 0.12
metadata_json = jsonencode({
"avg:redis.info.latency_ms{$host}": {
"alias": "Redis latency"
}
})
}
}
Expand Down Expand Up @@ -117,6 +125,30 @@ Nested `graph` `request` blocks have the following structure:
* `type` - (Optional) Choose how to draw the graph. For example: "line", "bars" or "area". Default: "line".
* `style` - (Optional) Nested block to customize the graph style.
* `conditional_format` - (Optional) Nested block to customize the graph style if certain conditions are met. Currently only applies to `Query Value` and `Top List` type graphs.
* `metadata_json` - (Optional) A JSON blob (preferrably created using [jsonencode](https://www.terraform.io/docs/configuration/functions/jsonencode.html)) representing mapping of query expressions to alias names. Note that the query expressions in `metadata_json` will be ignored if they're not present in the query. For example, this is how you define `metadata_json` with Terraform >= 0.12:
```
metadata_json = jsonencode({
"avg:redis.info.latency_ms{$host}": {
"alias": "Redis latency"
}
})
```
And here's how you define `metadata_json` with Terraform < 0.12:
```
variable "my_metadata" {
default = {
"avg:redis.info.latency_ms{$host}" = {
"alias": "Redis latency"
}
}
}
resource "datadog_timeboard" "SomeTimeboard" {
...
metadata_json = "${jsonencode(var.my_metadata)}"
}
```
Note that this has to be a JSON blob because of [limitations](https://github.com/hashicorp/terraform/issues/6215) of Terraform's handling complex nested structures. This is also why the key is called `metadata_json` even though it sets `metadata` attribute on the API call.

### Nested `graph` `style` block
The nested `style` block is used specifically for styling `hostmap` graphs, and has the following structure:
Expand Down

0 comments on commit fd7c536

Please sign in to comment.