Skip to content

Commit 356153b

Browse files
authored
Fix detection not working for collapsed rows (#18)
* Fix detection not working for collapsed rows * Fix collapsed rows detection
1 parent 5da6925 commit 356153b

File tree

4 files changed

+136
-75
lines changed

4 files changed

+136
-75
lines changed

api/grafana/grafana.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,20 @@ func (cl APIClient) GetDashboard(ctx context.Context, uid string) (*DashboardDef
4848
if err := cl.Request(ctx, http.MethodGet, "dashboards/uid/"+uid, &out); err != nil {
4949
return nil, err
5050
}
51-
// Convert datasources map[string]interface{} to custom type
52-
// The datasource field can either be a string (old) or object (new)
53-
// Could check for schema, but this is easier
54-
for _, panel := range out.Dashboard.Panels {
51+
convertPanels(out.Dashboard.Panels)
52+
return out, nil
53+
}
54+
55+
// convertPanels recursively converts datasources map[string]interface{} to custom type.
56+
// The datasource field can either be a string (old) or object (new).
57+
// Could check for schema, but this is easier.
58+
func convertPanels(panels []*DashboardPanel) {
59+
for _, panel := range panels {
60+
// Recurse
61+
if len(panel.Panels) > 0 {
62+
convertPanels(panel.Panels)
63+
}
64+
5565
m, ok := panel.Datasource.(map[string]interface{})
5666
if !ok {
5767
// String, keep as-is
@@ -65,8 +75,6 @@ func (cl APIClient) GetDashboard(ctx context.Context, uid string) (*DashboardDef
6575
}
6676
panel.Datasource = PanelDatasource{Type: m["type"].(string)}
6777
}
68-
69-
return out, nil
7078
}
7179

7280
func (cl APIClient) GetOrgs(ctx context.Context) ([]Org, error) {

api/grafana/models.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type DashboardPanel struct {
2828
Type string
2929
Title string
3030
Datasource interface{}
31+
32+
Panels []*DashboardPanel // present for collapsed rows
3133
}
3234

3335
type DashboardDefinition struct {

detector/detector.go

Lines changed: 117 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,38 @@ const (
1818
pluginIDTableOld = "table-old"
1919
)
2020

21+
// Detector can detect Angular plugins in Grafana dashboards.
22+
type Detector struct {
23+
log *logger.LeveledLogger
24+
grafanaClient grafana.APIClient
25+
gcomClient gcom.APIClient
26+
27+
angularDetected map[string]bool
28+
datasourcePluginIDs map[string]string
29+
}
30+
31+
// NewDetector returns a new Detector.
32+
func NewDetector(log *logger.LeveledLogger, grafanaClient grafana.APIClient, gcomClient gcom.APIClient) *Detector {
33+
return &Detector{
34+
log: log,
35+
grafanaClient: grafanaClient,
36+
gcomClient: gcomClient,
37+
}
38+
}
39+
2140
// Run runs the angular detector tool against the specified Grafana instance.
22-
func Run(ctx context.Context, log *logger.LeveledLogger, grafanaClient grafana.APIClient) ([]output.Dashboard, error) {
41+
func (d *Detector) Run(ctx context.Context) ([]output.Dashboard, error) {
2342
var (
2443
finalOutput []output.Dashboard
2544
// Determine if we should use GCOM or frontendsettings
2645
useGCOM bool
2746
)
2847

29-
gcomCl := gcom.NewAPIClient()
30-
3148
// Determine if plugins are angular.
3249
// This can be done from frontendsettings (faster and works with private plugins, but only works with >= 10.1.0)
3350
// or from GCOM (slower, but always available, but public plugins only)
34-
angularDetected := map[string]bool{}
35-
frontendSettings, err := grafanaClient.GetFrontendSettings(ctx)
51+
d.angularDetected = map[string]bool{}
52+
frontendSettings, err := d.grafanaClient.GetFrontendSettings(ctx)
3653
if err != nil {
3754
return []output.Dashboard{}, fmt.Errorf("get frontend settings: %w", err)
3855
}
@@ -49,19 +66,19 @@ func Run(ctx context.Context, log *logger.LeveledLogger, grafanaClient grafana.A
4966
}
5067
if useGCOM {
5168
// Fall back to GCOM (< 10.1.0)
52-
log.Verbose().Log("Using GCOM to find Angular plugins")
53-
log.Log("(WARNING, dependencies on private plugins won't be flagged)")
69+
d.log.Verbose().Log("Using GCOM to find Angular plugins")
70+
d.log.Log("(WARNING, dependencies on private plugins won't be flagged)")
5471

5572
// Double check that the token has the correct permissions, which is "datasources:create".
5673
// If we don't have such permissions, the plugins endpoint will still return a valid response,
5774
// but it will contain only core plugins:
5875
// https://github.com/grafana/grafana/blob/0315b911ef45b4ce9d3d5c182d8b112c6b9b41da/pkg/api/plugins.go#L56
59-
permissions, err := grafanaClient.GetServiceAccountPermissions(ctx)
76+
permissions, err := d.grafanaClient.GetServiceAccountPermissions(ctx)
6077
if err != nil {
6178
// Do not hard fail if we can't get service account permissions
6279
// as we may be running against an old Grafana version without service accounts
63-
log.Verbose().Log("(WARNING: could not get service account permissions: %v)", err)
64-
log.Verbose().Log("Please make sure that you have created an ADMIN token or the output will be wrong")
80+
d.log.Verbose().Log("(WARNING: could not get service account permissions: %v)", err)
81+
d.log.Verbose().Log("Please make sure that you have created an ADMIN token or the output will be wrong")
6582
} else {
6683
_, hasDsCreate := permissions["datasources:create"]
6784
_, hasPluginsInstall := permissions["plugins:install"]
@@ -74,122 +91,154 @@ func Run(ctx context.Context, log *logger.LeveledLogger, grafanaClient grafana.A
7491
}
7592

7693
// Get the plugins
77-
plugins, err := grafanaClient.GetPlugins(ctx)
94+
plugins, err := d.grafanaClient.GetPlugins(ctx)
7895
if err != nil {
7996
return []output.Dashboard{}, fmt.Errorf("get plugins: %w", err)
8097
}
8198
for _, p := range plugins {
8299
if p.Info.Version == "" {
83100
continue
84101
}
85-
angularDetected[p.ID], err = gcomCl.GetAngularDetected(ctx, p.ID, p.Info.Version)
102+
d.angularDetected[p.ID], err = d.gcomClient.GetAngularDetected(ctx, p.ID, p.Info.Version)
86103
if err != nil {
87104
return []output.Dashboard{}, fmt.Errorf("get angular detected: %w", err)
88105
}
89106
}
90107
} else {
91-
log.Verbose().Log("Using frontendsettings to find Angular plugins")
108+
d.log.Verbose().Log("Using frontendsettings to find Angular plugins")
92109
for pluginID, panel := range frontendSettings.Panels {
93110
v, err := panel.IsAngular()
94111
if err != nil {
95112
return []output.Dashboard{}, fmt.Errorf("%q is angular: %w", pluginID, err)
96113
}
97-
angularDetected[pluginID] = v
114+
d.angularDetected[pluginID] = v
98115
}
99116
for _, ds := range frontendSettings.Datasources {
100117
v, err := ds.IsAngular()
101118
if err != nil {
102119
return []output.Dashboard{}, fmt.Errorf("%q is angular: %w", ds.Type, err)
103120
}
104-
angularDetected[ds.Type] = v
121+
d.angularDetected[ds.Type] = v
105122
}
106123
}
107124

108125
// Debug
109-
for p, isAngular := range angularDetected {
110-
log.Verbose().Log("Plugin %q angular %t", p, isAngular)
126+
for p, isAngular := range d.angularDetected {
127+
d.log.Verbose().Log("Plugin %q angular %t", p, isAngular)
111128
}
112129

113130
// Map ds name -> ds plugin id, to resolve legacy dashboards that have ds name
114-
apiDs, err := grafanaClient.GetDatasourcePluginIDs(ctx)
131+
apiDs, err := d.grafanaClient.GetDatasourcePluginIDs(ctx)
115132
if err != nil {
116133
return []output.Dashboard{}, fmt.Errorf("get datasource plugin ids: %w", err)
117134
}
118-
datasourcePluginIDs := make(map[string]string, len(apiDs))
135+
d.datasourcePluginIDs = make(map[string]string, len(apiDs))
119136
for _, ds := range apiDs {
120-
datasourcePluginIDs[ds.Name] = ds.Type
137+
d.datasourcePluginIDs[ds.Name] = ds.Type
121138
}
122139

123-
dashboards, err := grafanaClient.GetDashboards(ctx, 1)
140+
dashboards, err := d.grafanaClient.GetDashboards(ctx, 1)
124141
if err != nil {
125142
return []output.Dashboard{}, fmt.Errorf("get dashboards: %w", err)
126143
}
127144

128-
for _, d := range dashboards {
145+
for _, dash := range dashboards {
129146
// Determine absolute dashboard URL for output
130-
dashboardAbsURL, err := url.JoinPath(strings.TrimSuffix(grafanaClient.BaseURL, "/api"), d.URL)
147+
dashboardAbsURL, err := url.JoinPath(strings.TrimSuffix(d.grafanaClient.BaseURL, "/api"), dash.URL)
131148
if err != nil {
132149
// Silently ignore errors
133150
dashboardAbsURL = ""
134151
}
135-
dashboardDefinition, err := grafanaClient.GetDashboard(ctx, d.UID)
152+
dashboardDefinition, err := d.grafanaClient.GetDashboard(ctx, dash.UID)
136153
if err != nil {
137-
return []output.Dashboard{}, fmt.Errorf("get dashboard %q: %w", d.UID, err)
154+
return []output.Dashboard{}, fmt.Errorf("get dashboard %q: %w", dash.UID, err)
138155
}
139156
dashboardOutput := output.Dashboard{
140157
Detections: []output.Detection{},
141158
URL: dashboardAbsURL,
142-
Title: d.Title,
159+
Title: dash.Title,
143160
Folder: dashboardDefinition.Meta.FolderTitle,
144161
CreatedBy: dashboardDefinition.Meta.CreatedBy,
145162
UpdatedBy: dashboardDefinition.Meta.UpdatedBy,
146163
Created: dashboardDefinition.Meta.Created,
147164
Updated: dashboardDefinition.Meta.Updated,
148165
}
149-
for _, p := range dashboardDefinition.Dashboard.Panels {
150-
// Check panel
151-
// - "graph" has been replaced with timeseries
152-
// - "table-old" is the old table panel (after it has been migrated)
153-
// - "table" with a schema version < 24 is Angular table panel, which will be replaced by `table-old`:
154-
// https://github.com/grafana/grafana/blob/7869ca1932c3a2a8f233acf35a3fe676187847bc/public/app/features/dashboard/state/DashboardMigrator.ts#L595-L610
155-
if p.Type == pluginIDGraphOld || p.Type == pluginIDTableOld || (p.Type == pluginIDTable && dashboardDefinition.Dashboard.SchemaVersion < 24) {
156-
// Different warning on legacy panel that can be migrated to React automatically
157-
dashboardOutput.Detections = append(dashboardOutput.Detections, output.Detection{
158-
DetectionType: output.DetectionTypeLegacyPanel,
159-
PluginID: p.Type,
160-
Title: p.Title,
161-
})
162-
} else if angularDetected[p.Type] {
163-
// Angular plugin
164-
dashboardOutput.Detections = append(dashboardOutput.Detections, output.Detection{
165-
DetectionType: output.DetectionTypePanel,
166-
PluginID: p.Type,
167-
Title: p.Title,
168-
})
169-
}
170-
171-
// Check datasource
172-
var dsPlugin string
173-
// The datasource field can either be a string (old) or object (new)
174-
if p.Datasource == nil || p.Datasource == "" {
175-
continue
176-
}
177-
if dsName, ok := p.Datasource.(string); ok {
178-
dsPlugin = datasourcePluginIDs[dsName]
179-
} else if ds, ok := p.Datasource.(grafana.PanelDatasource); ok {
180-
dsPlugin = ds.Type
181-
} else {
182-
return []output.Dashboard{}, fmt.Errorf("unknown unmarshaled datasource type %T", p.Datasource)
183-
}
184-
if angularDetected[dsPlugin] {
185-
dashboardOutput.Detections = append(dashboardOutput.Detections, output.Detection{
186-
DetectionType: output.DetectionTypeDatasource,
187-
PluginID: dsPlugin,
188-
Title: p.Title,
189-
})
190-
}
166+
dashboardOutput.Detections, err = d.checkPanels(dashboardDefinition, dashboardDefinition.Dashboard.Panels)
167+
if err != nil {
168+
return []output.Dashboard{}, fmt.Errorf("check panels: %w", err)
191169
}
192170
finalOutput = append(finalOutput, dashboardOutput)
193171
}
194172
return finalOutput, nil
195173
}
174+
175+
// checkPanels calls checkPanel recursively on the given panels.
176+
func (d *Detector) checkPanels(dashboardDefinition *grafana.DashboardDefinition, panels []*grafana.DashboardPanel) ([]output.Detection, error) {
177+
var out []output.Detection
178+
for _, p := range panels {
179+
r, err := d.checkPanel(dashboardDefinition, p)
180+
if err != nil {
181+
return nil, err
182+
}
183+
out = append(out, r...)
184+
185+
// Recurse
186+
if len(p.Panels) == 0 {
187+
continue
188+
}
189+
rr, err := d.checkPanels(dashboardDefinition, p.Panels)
190+
if err != nil {
191+
return nil, err
192+
}
193+
out = append(out, rr...)
194+
}
195+
return out, nil
196+
}
197+
198+
// checkPanel checks the given panel for Angular plugins.
199+
func (d *Detector) checkPanel(dashboardDefinition *grafana.DashboardDefinition, p *grafana.DashboardPanel) ([]output.Detection, error) {
200+
var out []output.Detection
201+
202+
// Check panel
203+
// - "graph" has been replaced with timeseries
204+
// - "table-old" is the old table panel (after it has been migrated)
205+
// - "table" with a schema version < 24 is Angular table panel, which will be replaced by `table-old`:
206+
// https://github.com/grafana/grafana/blob/7869ca1932c3a2a8f233acf35a3fe676187847bc/public/app/features/dashboard/state/DashboardMigrator.ts#L595-L610
207+
if p.Type == pluginIDGraphOld || p.Type == pluginIDTableOld || (p.Type == pluginIDTable && dashboardDefinition.Dashboard.SchemaVersion < 24) {
208+
// Different warning on legacy panel that can be migrated to React automatically
209+
out = append(out, output.Detection{
210+
DetectionType: output.DetectionTypeLegacyPanel,
211+
PluginID: p.Type,
212+
Title: p.Title,
213+
})
214+
} else if d.angularDetected[p.Type] {
215+
// Angular plugin
216+
out = append(out, output.Detection{
217+
DetectionType: output.DetectionTypePanel,
218+
PluginID: p.Type,
219+
Title: p.Title,
220+
})
221+
}
222+
223+
// Check datasource
224+
var dsPlugin string
225+
// The datasource field can either be a string (old) or object (new)
226+
if p.Datasource == nil || p.Datasource == "" {
227+
return out, nil
228+
}
229+
if dsName, ok := p.Datasource.(string); ok {
230+
dsPlugin = d.datasourcePluginIDs[dsName]
231+
} else if ds, ok := p.Datasource.(grafana.PanelDatasource); ok {
232+
dsPlugin = ds.Type
233+
} else {
234+
return nil, fmt.Errorf("unknown unmarshaled datasource type %T", p.Datasource)
235+
}
236+
if d.angularDetected[dsPlugin] {
237+
out = append(out, output.Detection{
238+
DetectionType: output.DetectionTypeDatasource,
239+
PluginID: dsPlugin,
240+
Title: p.Title,
241+
})
242+
}
243+
return out, nil
244+
}

main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010

1111
"github.com/grafana/detect-angular-dashboards/api"
12+
"github.com/grafana/detect-angular-dashboards/api/gcom"
1213
"github.com/grafana/detect-angular-dashboards/api/grafana"
1314
"github.com/grafana/detect-angular-dashboards/build"
1415
"github.com/grafana/detect-angular-dashboards/detector"
@@ -74,7 +75,8 @@ func main() {
7475
}))
7576
}
7677
client := grafana.NewAPIClient(api.NewClient(grafanaURL, opts...))
77-
finalOutput, err := detector.Run(ctx, log, client)
78+
d := detector.NewDetector(log, client, gcom.NewAPIClient())
79+
finalOutput, err := d.Run(ctx)
7880
if err != nil {
7981
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err)
8082
os.Exit(0)

0 commit comments

Comments
 (0)