@@ -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+ }
0 commit comments