Skip to content

Commit 6102ee9

Browse files
authored
feat: Add dashboard scopes (#90)
* feat: Add dashboard scope management * docs: Add scoping information for dashboards * style: Format files * fix: Do not read response body on failed request
1 parent 48cadee commit 6102ee9

File tree

5 files changed

+209
-31
lines changed

5 files changed

+209
-31
lines changed

sysdig/internal/client/monitor/model/dashboard.go

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -316,32 +316,42 @@ type TeamSharingOptions struct {
316316
UserTeamsRole string `json:"userTeamsRole"`
317317
SelectedTeams []interface{} `json:"selectedTeams"`
318318
}
319+
320+
type ScopeExpressionList struct {
321+
Operand string `json:"operand"`
322+
Operator string `json:"operator"`
323+
DisplayName string `json:"displayName"`
324+
Value []string `json:"value"`
325+
Descriptor interface{} `json:"descriptor"`
326+
IsVariable bool `json:"isVariable"`
327+
}
328+
319329
type Dashboard struct {
320-
Version int `json:"version,omitempty"`
321-
CustomerID interface{} `json:"customerId"`
322-
TeamID int `json:"teamId"`
323-
Schema int `json:"schema"`
324-
AutoCreated bool `json:"autoCreated"`
325-
PublicToken string `json:"publicToken"`
326-
ScopeExpressionList interface{} `json:"scopeExpressionList"`
327-
Layout []*Layout `json:"layout"`
328-
TeamScope interface{} `json:"teamScope"`
329-
EventDisplaySettings EventDisplaySettings `json:"eventDisplaySettings"`
330-
ID int `json:"id,omitempty"`
331-
Name string `json:"name"`
332-
Description string `json:"description"`
333-
Username string `json:"username"`
334-
Shared bool `json:"shared"`
335-
SharingSettings []interface{} `json:"sharingSettings"`
336-
Public bool `json:"public"`
337-
Favorite bool `json:"favorite"`
338-
CreatedOn int64 `json:"createdOn"`
339-
ModifiedOn int64 `json:"modifiedOn"`
340-
Panels []*Panels `json:"panels"`
341-
TeamScopeExpressionList []interface{} `json:"teamScopeExpressionList"`
342-
CreatedOnDate string `json:"createdOnDate"`
343-
ModifiedOnDate string `json:"modifiedOnDate"`
344-
TeamSharingOptions TeamSharingOptions `json:"teamSharingOptions"`
330+
Version int `json:"version,omitempty"`
331+
CustomerID interface{} `json:"customerId"`
332+
TeamID int `json:"teamId"`
333+
Schema int `json:"schema"`
334+
AutoCreated bool `json:"autoCreated"`
335+
PublicToken string `json:"publicToken"`
336+
ScopeExpressionList []*ScopeExpressionList `json:"scopeExpressionList"`
337+
Layout []*Layout `json:"layout"`
338+
TeamScope interface{} `json:"teamScope"`
339+
EventDisplaySettings EventDisplaySettings `json:"eventDisplaySettings"`
340+
ID int `json:"id,omitempty"`
341+
Name string `json:"name"`
342+
Description string `json:"description"`
343+
Username string `json:"username"`
344+
Shared bool `json:"shared"`
345+
SharingSettings []interface{} `json:"sharingSettings"`
346+
Public bool `json:"public"`
347+
Favorite bool `json:"favorite"`
348+
CreatedOn int64 `json:"createdOn"`
349+
ModifiedOn int64 `json:"modifiedOn"`
350+
Panels []*Panels `json:"panels"`
351+
TeamScopeExpressionList []interface{} `json:"teamScopeExpressionList"`
352+
CreatedOnDate string `json:"createdOnDate"`
353+
ModifiedOnDate string `json:"modifiedOnDate"`
354+
TeamSharingOptions TeamSharingOptions `json:"teamSharingOptions"`
345355
}
346356

347357
type dashboardWrapper struct {

sysdig/internal/client/secure/rules.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,15 @@ func (client *sysdigSecureClient) CreateRule(ctx context.Context, rule Rule) (re
3737
}
3838

3939
func (client *sysdigSecureClient) GetRuleByID(ctx context.Context, ruleID int) (result Rule, err error) {
40-
response, _ := client.doSysdigSecureRequest(ctx, http.MethodGet, client.ruleURL(ruleID), nil)
41-
body, _ := ioutil.ReadAll(response.Body)
40+
response, err := client.doSysdigSecureRequest(ctx, http.MethodGet, client.ruleURL(ruleID), nil)
41+
if err != nil {
42+
return
43+
}
44+
45+
body, err := ioutil.ReadAll(response.Body)
46+
if err != nil {
47+
return
48+
}
4249

4350
if response.StatusCode != 200 {
4451
return Rule{}, errors.New(string(body))
@@ -51,8 +58,14 @@ func (client *sysdigSecureClient) GetRuleByID(ctx context.Context, ruleID int) (
5158
}
5259

5360
func (client *sysdigSecureClient) UpdateRule(ctx context.Context, rule Rule) (result Rule, err error) {
54-
response, _ := client.doSysdigSecureRequest(ctx, http.MethodPut, client.ruleURL(rule.ID), rule.ToJSON())
55-
body, _ := ioutil.ReadAll(response.Body)
61+
response, err := client.doSysdigSecureRequest(ctx, http.MethodPut, client.ruleURL(rule.ID), rule.ToJSON())
62+
if err != nil {
63+
return
64+
}
65+
body, err := ioutil.ReadAll(response.Body)
66+
if err != nil {
67+
return
68+
}
5669

5770
if response.StatusCode != http.StatusOK {
5871
return Rule{}, errors.New(string(body))

sysdig/resource_sysdig_monitor_dashboard.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sysdig
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"strconv"
78
"time"
@@ -52,6 +53,34 @@ func resourceSysdigMonitorDashboard() *schema.Resource {
5253
ComputedWhen: []string{"public"},
5354
Computed: true,
5455
},
56+
"scope": {
57+
Type: schema.TypeSet,
58+
Optional: true,
59+
Elem: &schema.Resource{
60+
Schema: map[string]*schema.Schema{
61+
"metric": {
62+
Type: schema.TypeString,
63+
Required: true,
64+
},
65+
"comparator": {
66+
Type: schema.TypeString,
67+
Optional: true,
68+
ValidateDiagFunc: validateDiagFunc(validation.StringInSlice([]string{"in", "notIn", "equals", "notEquals", "contains", "notContains", "startsWith"}, false)),
69+
},
70+
"value": {
71+
Type: schema.TypeList,
72+
Elem: &schema.Schema{
73+
Type: schema.TypeString,
74+
},
75+
Optional: true,
76+
},
77+
"variable": {
78+
Type: schema.TypeString,
79+
Optional: true,
80+
},
81+
},
82+
},
83+
},
5584
"panel": {
5685
Type: schema.TypeSet,
5786
Required: true,
@@ -236,6 +265,12 @@ func dashboardFromResourceData(data *schema.ResourceData) (dashboard *model.Dash
236265
return nil, err
237266
}
238267

268+
scopes, err := scopeFromResourceData(data)
269+
if err != nil {
270+
return nil, err
271+
}
272+
dashboard.ScopeExpressionList = scopes
273+
239274
dashboard.AddPanels(panels...)
240275
return dashboard, nil
241276
}
@@ -473,11 +508,75 @@ func dashboardToResourceData(dashboard *model.Dashboard, data *schema.ResourceDa
473508
panels = append(panels, dPanel)
474509
}
475510
data.Set("panel", panels)
511+
512+
var scopes []map[string]interface{}
513+
for _, scope := range dashboard.ScopeExpressionList {
514+
dScope, err := scopeToResourceData(scope)
515+
if err != nil {
516+
return err
517+
}
518+
scopes = append(scopes, dScope)
519+
}
520+
data.Set("scope", scopes)
521+
476522
data.Set("version", dashboard.Version)
477523

478524
return nil
479525
}
480526

527+
func scopeToResourceData(scope *model.ScopeExpressionList) (map[string]interface{}, error) {
528+
res := map[string]interface{}{
529+
"metric": scope.Operand,
530+
}
531+
532+
if len(scope.Value) > 0 {
533+
res["value"] = scope.Value
534+
res["comparator"] = scope.Operator
535+
}
536+
537+
if scope.IsVariable && scope.DisplayName != "" {
538+
res["variable"] = scope.DisplayName
539+
}
540+
541+
return res, nil
542+
}
543+
544+
func scopeFromResourceData(data *schema.ResourceData) ([]*model.ScopeExpressionList, error) {
545+
scopes := []*model.ScopeExpressionList{}
546+
for _, scopeItr := range data.Get("scope").(*schema.Set).List() {
547+
scopeInfo := (scopeItr).(map[string]interface{})
548+
549+
scope := &model.ScopeExpressionList{}
550+
scope.Operand = cast.ToString(scopeInfo["metric"])
551+
scope.Value = []string{}
552+
comparator := cast.ToString(scopeInfo["comparator"])
553+
value := cast.ToStringSlice(scopeInfo["value"])
554+
if comparator != "" {
555+
scope.Operator = comparator
556+
if len(value) == 0 {
557+
return nil, errors.New(`"value" field is required if the comparator is set up`)
558+
}
559+
if scope.Operator != "in" && scope.Operator != "notIn" && len(value) > 1 {
560+
return nil, errors.New(`"value" can only contain 1 value if the "comparator" is not "in" and "notIn"`)
561+
}
562+
scope.Value = value
563+
}
564+
variable := cast.ToString(scopeInfo["variable"])
565+
if variable != "" {
566+
scope.DisplayName = variable
567+
scope.IsVariable = true
568+
if scope.Operator == "" {
569+
scope.Operator = "in"
570+
}
571+
} else if comparator == "" || len(value) == 0 {
572+
return nil, errors.New(`"comparator" and "value" must be set if "variable" is not set`)
573+
}
574+
575+
scopes = append(scopes, scope)
576+
}
577+
return scopes, nil
578+
}
579+
481580
func panelToResourceData(panel *model.Panels, layout []*model.Layout) (map[string]interface{}, error) {
482581
var panelLayout *model.Layout
483582

sysdig/resource_sysdig_monitor_dashboard_test.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,24 @@ resource "sysdig_monitor_dashboard" "dashboard" {
102102
name = "TERRAFORM TEST - METRIC %s"
103103
description = "TERRAFORM TEST - METRIC %s"
104104
105+
scope {
106+
metric = "agent.id"
107+
comparator = "in"
108+
value = ["foo", "bar"]
109+
variable = "agent_id"
110+
}
111+
112+
scope {
113+
metric = "agent.name"
114+
comparator = "equals"
115+
value = ["name"]
116+
}
117+
118+
scope {
119+
metric = "kubernetes.namespace.name"
120+
variable = "k8_ns"
121+
}
122+
105123
panel {
106124
pos_x = 0
107125
pos_y = 0
@@ -116,7 +134,7 @@ resource "sysdig_monitor_dashboard" "dashboard" {
116134
unit = "percent"
117135
}
118136
query {
119-
promql = "avg(avg_over_time(sysdig_host_cpu_used_percent[$__interval]))"
137+
promql = "avg(avg_over_time(sysdig_host_cpu_used_percent{ns_name=$k8s_ns}[$__interval]))"
120138
unit = "number"
121139
}
122140
}

website/docs/r/sysdig_monitor_dashboard.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ resource "sysdig_monitor_dashboard" "dashboard" {
1919
name = "Example Dashboard"
2020
description = "Example Dashboard description"
2121
22+
scope {
23+
metric = "kubernetes.cluster.name"
24+
comparator = "in"
25+
value = ["prod", "dev"]
26+
variable = "cluster_name"
27+
}
28+
29+
scope {
30+
metric = "host.hostName"
31+
variable = "hostname"
32+
}
33+
2234
panel {
2335
pos_x = 0
2436
pos_y = 0
@@ -29,7 +41,7 @@ resource "sysdig_monitor_dashboard" "dashboard" {
2941
description = "Description"
3042
3143
query {
32-
promql = "avg(avg_over_time(sysdig_host_cpu_used_percent[$__interval]))"
44+
promql = "avg_over_time(sysdig_host_cpu_used_percent{host_name=$hostname}[$__interval])"
3345
unit = "percent"
3446
}
3547
query {
@@ -77,9 +89,28 @@ resource "sysdig_monitor_dashboard" "dashboard" {
7789
* `description` - (Optional) Description of the dashboard.
7890

7991
* `public` - (Optional) Define if the dashboard can be accessible without requiring the user to be logged in.
92+
93+
* `scope` - (Optional) Define the scope of the dashboard and variables for these metrics.
8094

8195
* `panel` - (Required) At least 1 panel is required to define a Dashboard.
8296

97+
98+
### scope
99+
100+
Dashboard scope defines what data is valid for aggregation and display within the dashboard.
101+
See more info about how to [use the scope in a PromQL query](https://docs.sysdig.com/en/using-promql.html#UUID-2314cf2d-3466-d7a5-142a-30a9e63053d0_UUID-8dfed5eb-8c48-8f94-4e3a-61b051fb9b440) in the official documentation.
102+
103+
The following arguments are supported to configure a scope:
104+
105+
* `metric` - (Required) Metric to scope by, common examples are `host.hostName`, `kubernetes.namespace.name` or `kubernetes.cluster.name`, but you can use all the Sysdig-supported values shown in the UI. Note that kubernetes-related values only appear when Sysdig detects Kubernetes metadata.
106+
107+
* `comparator` - (Optional) Operator to relate the metric with some value. It is only required if the value to filter by is set, or the variable field is not set. Valid values are: `in`, `notIn`, `equals`, `notEquals`, `contains`, `notContains` and `startsWith`.
108+
109+
* `value` - (Optional) List of values to filter by, if comparator is set. If the comparator is not `in` or `notIn` the list must contain only 1 value.
110+
111+
* `variable` - (Optional) Assigns this metric to a value name and allows PromQL to reference it.
112+
113+
83114
### panel
84115

85116
The whole screen for a dashboard is separated in 24 squares of width. All the panels must not
@@ -123,6 +154,13 @@ The following arguments are supported:
123154

124155
### query
125156

157+
To scope a panel built from a PromQL query, you must use a scope variable within the query. The variable will take the value of the referenced scope parameter, and the PromQL panel will change accordingly.
158+
There are two predefined variables available:
159+
160+
- `$__interval` represents the time interval defined based on the time range. This will help to adapt the time range for different operations, such as rate and avg_over_time, and prevent displaying empty graphs due to the change in the granularity of the data.
161+
162+
- `$__range` represents the time interval defined for the dashboard. This is used to adapt operations like calculating average for a time frame selected.
163+
126164
The following arguments are supported:
127165

128166
* `promql` - (Required) The PromQL query. Must be a valid PromQL query with existing

0 commit comments

Comments
 (0)