@intentius/chant-lexicon-grafana 0.97.0 → 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/README.md +15 -5
  2. package/dist/actions/grafana.d.ts +24 -0
  3. package/dist/actions/grafana.d.ts.map +1 -0
  4. package/dist/actions/index.d.ts +2 -0
  5. package/dist/actions/index.d.ts.map +1 -0
  6. package/dist/alerting-build.d.ts +128 -0
  7. package/dist/alerting-build.d.ts.map +1 -0
  8. package/dist/alerting.d.ts +273 -0
  9. package/dist/alerting.d.ts.map +1 -0
  10. package/dist/api/apply.d.ts +156 -0
  11. package/dist/api/apply.d.ts.map +1 -0
  12. package/dist/api/bind.d.ts +32 -0
  13. package/dist/api/bind.d.ts.map +1 -0
  14. package/dist/api/client.d.ts +76 -0
  15. package/dist/api/client.d.ts.map +1 -0
  16. package/dist/api/converge.d.ts +33 -0
  17. package/dist/api/converge.d.ts.map +1 -0
  18. package/dist/api/dashboards.d.ts +102 -0
  19. package/dist/api/dashboards.d.ts.map +1 -0
  20. package/dist/api/datasources.d.ts +26 -0
  21. package/dist/api/datasources.d.ts.map +1 -0
  22. package/dist/api/fake-grafana-writes.d.ts +41 -0
  23. package/dist/api/fake-grafana-writes.d.ts.map +1 -0
  24. package/dist/api/fake-grafana.d.ts +61 -0
  25. package/dist/api/fake-grafana.d.ts.map +1 -0
  26. package/dist/api/folders.d.ts +110 -0
  27. package/dist/api/folders.d.ts.map +1 -0
  28. package/dist/api/library-panels.d.ts +52 -0
  29. package/dist/api/library-panels.d.ts.map +1 -0
  30. package/dist/build.d.ts +30 -2
  31. package/dist/build.d.ts.map +1 -1
  32. package/dist/catalog.d.ts +1 -1
  33. package/dist/catalog.d.ts.map +1 -1
  34. package/dist/codegen/docs.d.ts.map +1 -1
  35. package/dist/codegen/schema-types.d.ts +3 -3
  36. package/dist/codegen/schema-types.d.ts.map +1 -1
  37. package/dist/composites/agent-dashboard.d.ts.map +1 -1
  38. package/dist/composites/catalog.d.ts.map +1 -1
  39. package/dist/composites/index.d.ts +4 -2
  40. package/dist/composites/index.d.ts.map +1 -1
  41. package/dist/composites/red-dashboard.d.ts +21 -2
  42. package/dist/composites/red-dashboard.d.ts.map +1 -1
  43. package/dist/composites/shared.d.ts +7 -0
  44. package/dist/composites/shared.d.ts.map +1 -1
  45. package/dist/composites/slo-alert-rules.d.ts +92 -0
  46. package/dist/composites/slo-alert-rules.d.ts.map +1 -0
  47. package/dist/config.d.ts +111 -0
  48. package/dist/config.d.ts.map +1 -0
  49. package/dist/contact-point-secrets.d.ts +17 -0
  50. package/dist/contact-point-secrets.d.ts.map +1 -0
  51. package/dist/dashboard.d.ts +16 -2
  52. package/dist/dashboard.d.ts.map +1 -1
  53. package/dist/datasource-refs.d.ts +95 -0
  54. package/dist/datasource-refs.d.ts.map +1 -0
  55. package/dist/datasource.d.ts +29 -0
  56. package/dist/datasource.d.ts.map +1 -1
  57. package/dist/deep-observe-hooks.d.ts +64 -0
  58. package/dist/deep-observe-hooks.d.ts.map +1 -0
  59. package/dist/deep-observe.d.ts +42 -0
  60. package/dist/deep-observe.d.ts.map +1 -0
  61. package/dist/describe-resources.d.ts +49 -0
  62. package/dist/describe-resources.d.ts.map +1 -0
  63. package/dist/detect.d.ts +28 -1
  64. package/dist/detect.d.ts.map +1 -1
  65. package/dist/export-resources.d.ts +38 -0
  66. package/dist/export-resources.d.ts.map +1 -0
  67. package/dist/import/alerting.d.ts +40 -0
  68. package/dist/import/alerting.d.ts.map +1 -0
  69. package/dist/import/edits.d.ts +47 -0
  70. package/dist/import/edits.d.ts.map +1 -0
  71. package/dist/import/embedded.d.ts +14 -0
  72. package/dist/import/embedded.d.ts.map +1 -0
  73. package/dist/import/generator.d.ts +60 -0
  74. package/dist/import/generator.d.ts.map +1 -0
  75. package/dist/import/live-export.d.ts +108 -0
  76. package/dist/import/live-export.d.ts.map +1 -0
  77. package/dist/import/mappings.d.ts +83 -0
  78. package/dist/import/mappings.d.ts.map +1 -0
  79. package/dist/import/model.d.ts +98 -0
  80. package/dist/import/model.d.ts.map +1 -0
  81. package/dist/import/normalize-alerting.d.ts +26 -0
  82. package/dist/import/normalize-alerting.d.ts.map +1 -0
  83. package/dist/import/normalize.d.ts +74 -0
  84. package/dist/import/normalize.d.ts.map +1 -0
  85. package/dist/import/parser.d.ts +110 -0
  86. package/dist/import/parser.d.ts.map +1 -0
  87. package/dist/import/report.d.ts +26 -0
  88. package/dist/import/report.d.ts.map +1 -0
  89. package/dist/import/testdata/fixtures.d.ts +35 -0
  90. package/dist/import/testdata/fixtures.d.ts.map +1 -0
  91. package/dist/import/v2.d.ts +72 -0
  92. package/dist/import/v2.d.ts.map +1 -0
  93. package/dist/index.d.ts +26 -9
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/integrity.json +18 -10
  96. package/dist/k8s.d.ts +107 -0
  97. package/dist/k8s.d.ts.map +1 -0
  98. package/dist/lint/audit-catalog.d.ts +2 -1
  99. package/dist/lint/audit-catalog.d.ts.map +1 -1
  100. package/dist/lint/post-synth/graf101.d.ts +2 -2
  101. package/dist/lint/post-synth/graf102.d.ts +1 -1
  102. package/dist/lint/post-synth/graf107.d.ts +1 -1
  103. package/dist/lint/post-synth/graf108.d.ts +8 -0
  104. package/dist/lint/post-synth/graf108.d.ts.map +1 -0
  105. package/dist/lint/post-synth/graf109.d.ts +8 -0
  106. package/dist/lint/post-synth/graf109.d.ts.map +1 -0
  107. package/dist/lint/post-synth/graf111.d.ts +8 -0
  108. package/dist/lint/post-synth/graf111.d.ts.map +1 -0
  109. package/dist/lint/post-synth/graf112.d.ts +8 -0
  110. package/dist/lint/post-synth/graf112.d.ts.map +1 -0
  111. package/dist/lint/post-synth/graf113.d.ts +8 -0
  112. package/dist/lint/post-synth/graf113.d.ts.map +1 -0
  113. package/dist/lint/post-synth/graf114.d.ts +8 -0
  114. package/dist/lint/post-synth/graf114.d.ts.map +1 -0
  115. package/dist/lint/post-synth/graf115.d.ts +8 -0
  116. package/dist/lint/post-synth/graf115.d.ts.map +1 -0
  117. package/dist/lint/post-synth/grafana-helpers.d.ts +7 -5
  118. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
  119. package/dist/lint/post-synth/index.d.ts.map +1 -1
  120. package/dist/lint/rules/literal-secret.d.ts +7 -3
  121. package/dist/lint/rules/literal-secret.d.ts.map +1 -1
  122. package/dist/lsp/hover.d.ts.map +1 -1
  123. package/dist/manifest.json +1 -1
  124. package/dist/members.d.ts +27 -0
  125. package/dist/members.d.ts.map +1 -0
  126. package/dist/meta.json +135 -0
  127. package/dist/okf/index.md +36 -2
  128. package/dist/okf/rules/GRAF002.md +3 -2
  129. package/dist/okf/rules/GRAF101.md +3 -2
  130. package/dist/okf/rules/GRAF102.md +1 -0
  131. package/dist/okf/rules/GRAF108.md +11 -0
  132. package/dist/okf/rules/GRAF109.md +15 -0
  133. package/dist/okf/rules/GRAF111.md +11 -0
  134. package/dist/okf/rules/GRAF112.md +16 -0
  135. package/dist/okf/rules/GRAF113.md +16 -0
  136. package/dist/okf/rules/GRAF114.md +11 -0
  137. package/dist/okf/rules/GRAF115.md +11 -0
  138. package/dist/okf/types/AlertListPanel.md +9 -0
  139. package/dist/okf/types/AlertQuery.md +9 -0
  140. package/dist/okf/types/AlertRule.md +9 -0
  141. package/dist/okf/types/AlertRuleGroup.md +9 -0
  142. package/dist/okf/types/BarChartPanel.md +9 -0
  143. package/dist/okf/types/BarGaugePanel.md +9 -0
  144. package/dist/okf/types/CanvasPanel.md +9 -0
  145. package/dist/okf/types/ClassicConditionsExpression.md +9 -0
  146. package/dist/okf/types/ContactPoint.md +14 -0
  147. package/dist/okf/types/Dashboard.md +1 -0
  148. package/dist/okf/types/Datasource.md +3 -2
  149. package/dist/okf/types/ExternalDatasource.md +15 -0
  150. package/dist/okf/types/FlameGraphPanel.md +9 -0
  151. package/dist/okf/types/GeomapPanel.md +9 -0
  152. package/dist/okf/types/HistogramPanel.md +9 -0
  153. package/dist/okf/types/MathExpression.md +9 -0
  154. package/dist/okf/types/MuteTiming.md +13 -0
  155. package/dist/okf/types/NodeGraphPanel.md +9 -0
  156. package/dist/okf/types/NotificationPolicy.md +9 -0
  157. package/dist/okf/types/NotificationTemplate.md +9 -0
  158. package/dist/okf/types/PieChartPanel.md +9 -0
  159. package/dist/okf/types/ReduceExpression.md +9 -0
  160. package/dist/okf/types/ResampleExpression.md +9 -0
  161. package/dist/okf/types/SqlExpression.md +9 -0
  162. package/dist/okf/types/StateTimelinePanel.md +9 -0
  163. package/dist/okf/types/StatusHistoryPanel.md +9 -0
  164. package/dist/okf/types/ThresholdExpression.md +9 -0
  165. package/dist/okf/types/TrendPanel.md +9 -0
  166. package/dist/okf/types/XYChartPanel.md +9 -0
  167. package/dist/op/activities/grafana-apply.d.ts +75 -0
  168. package/dist/op/activities/grafana-apply.d.ts.map +1 -0
  169. package/dist/op/activities/index.d.ts +9 -0
  170. package/dist/op/activities/index.d.ts.map +1 -0
  171. package/dist/op/builders.d.ts +24 -0
  172. package/dist/op/builders.d.ts.map +1 -0
  173. package/dist/ownership.d.ts +67 -0
  174. package/dist/ownership.d.ts.map +1 -0
  175. package/dist/panel-options.d.ts +65 -0
  176. package/dist/panel-options.d.ts.map +1 -0
  177. package/dist/panels.d.ts +46 -9
  178. package/dist/panels.d.ts.map +1 -1
  179. package/dist/pin.d.ts +29 -7
  180. package/dist/pin.d.ts.map +1 -1
  181. package/dist/plugin.d.ts.map +1 -1
  182. package/dist/promql-check.d.ts +51 -0
  183. package/dist/promql-check.d.ts.map +1 -0
  184. package/dist/query.d.ts +9 -8
  185. package/dist/query.d.ts.map +1 -1
  186. package/dist/rules/graf101.ts +3 -3
  187. package/dist/rules/graf102.ts +1 -1
  188. package/dist/rules/graf107.ts +1 -1
  189. package/dist/rules/graf108.ts +17 -0
  190. package/dist/rules/graf109.ts +17 -0
  191. package/dist/rules/graf111.ts +17 -0
  192. package/dist/rules/graf112.ts +17 -0
  193. package/dist/rules/graf113.ts +17 -0
  194. package/dist/rules/graf114.ts +17 -0
  195. package/dist/rules/graf115.ts +17 -0
  196. package/dist/rules/grafana-helpers.ts +15 -8
  197. package/dist/rules/literal-secret.ts +60 -6
  198. package/dist/schema/barchart.gen.d.ts +198 -0
  199. package/dist/schema/barchart.gen.d.ts.map +1 -0
  200. package/dist/schema/bargauge.gen.d.ts +97 -0
  201. package/dist/schema/bargauge.gen.d.ts.map +1 -0
  202. package/dist/schema/canvas.gen.d.ts +152 -0
  203. package/dist/schema/canvas.gen.d.ts.map +1 -0
  204. package/dist/schema/dashboard.gen.d.ts +196 -21
  205. package/dist/schema/dashboard.gen.d.ts.map +1 -1
  206. package/dist/schema/expr.gen.d.ts +606 -0
  207. package/dist/schema/expr.gen.d.ts.map +1 -0
  208. package/dist/schema/gauge.gen.d.ts.map +1 -1
  209. package/dist/schema/geomap.gen.d.ts +100 -0
  210. package/dist/schema/geomap.gen.d.ts.map +1 -0
  211. package/dist/schema/heatmap.gen.d.ts.map +1 -1
  212. package/dist/schema/histogram.gen.d.ts +132 -0
  213. package/dist/schema/histogram.gen.d.ts.map +1 -0
  214. package/dist/schema/index.d.ts +12 -0
  215. package/dist/schema/index.d.ts.map +1 -1
  216. package/dist/schema/logs.gen.d.ts +1 -0
  217. package/dist/schema/logs.gen.d.ts.map +1 -1
  218. package/dist/schema/loki.gen.d.ts.map +1 -1
  219. package/dist/schema/nodegraph.gen.d.ts +49 -0
  220. package/dist/schema/nodegraph.gen.d.ts.map +1 -0
  221. package/dist/schema/piechart.gen.d.ts +127 -0
  222. package/dist/schema/piechart.gen.d.ts.map +1 -0
  223. package/dist/schema/prometheus.gen.d.ts.map +1 -1
  224. package/dist/schema/stat.gen.d.ts.map +1 -1
  225. package/dist/schema/statetimeline.gen.d.ts +124 -0
  226. package/dist/schema/statetimeline.gen.d.ts.map +1 -0
  227. package/dist/schema/statushistory.gen.d.ts +114 -0
  228. package/dist/schema/statushistory.gen.d.ts.map +1 -0
  229. package/dist/schema/table.gen.d.ts +22 -3
  230. package/dist/schema/table.gen.d.ts.map +1 -1
  231. package/dist/schema/tempo.gen.d.ts.map +1 -1
  232. package/dist/schema/text.gen.d.ts.map +1 -1
  233. package/dist/schema/timeseries.gen.d.ts +5 -1
  234. package/dist/schema/timeseries.gen.d.ts.map +1 -1
  235. package/dist/schema/trend.gen.d.ts +180 -0
  236. package/dist/schema/trend.gen.d.ts.map +1 -0
  237. package/dist/schema/xychart.gen.d.ts +145 -0
  238. package/dist/schema/xychart.gen.d.ts.map +1 -0
  239. package/dist/schema-validate.d.ts +30 -4
  240. package/dist/schema-validate.d.ts.map +1 -1
  241. package/dist/serializer.d.ts +11 -3
  242. package/dist/serializer.d.ts.map +1 -1
  243. package/dist/skill-defs.d.ts.map +1 -1
  244. package/dist/skills/chant-grafana-alerting.md +49 -0
  245. package/dist/skills/chant-grafana-provisioning.md +3 -3
  246. package/dist/skills/chant-grafana.md +12 -6
  247. package/dist/spec/fetch-units-cli.d.ts +3 -0
  248. package/dist/spec/fetch-units-cli.d.ts.map +1 -0
  249. package/dist/spec/overlay.d.ts +43 -0
  250. package/dist/spec/overlay.d.ts.map +1 -0
  251. package/dist/spec/schemas.d.ts +13 -7
  252. package/dist/spec/schemas.d.ts.map +1 -1
  253. package/dist/spec/units.d.ts +35 -0
  254. package/dist/spec/units.d.ts.map +1 -0
  255. package/dist/spec/units.gen.d.ts +2 -0
  256. package/dist/spec/units.gen.d.ts.map +1 -0
  257. package/dist/transformations.d.ts +459 -0
  258. package/dist/transformations.d.ts.map +1 -0
  259. package/dist/util.d.ts +6 -0
  260. package/dist/util.d.ts.map +1 -1
  261. package/dist/validate-alerting.d.ts +65 -0
  262. package/dist/validate-alerting.d.ts.map +1 -0
  263. package/dist/validate-output.d.ts +49 -9
  264. package/dist/validate-output.d.ts.map +1 -1
  265. package/dist/validate.d.ts.map +1 -1
  266. package/dist/variables.d.ts +7 -2
  267. package/dist/variables.d.ts.map +1 -1
  268. package/package.json +12 -4
  269. package/src/actions/actions.test.ts +24 -0
  270. package/src/actions/grafana.ts +40 -0
  271. package/src/actions/index.ts +1 -0
  272. package/src/alerting-build.ts +376 -0
  273. package/src/alerting.e2e.test.ts +212 -0
  274. package/src/alerting.test.ts +272 -0
  275. package/src/alerting.ts +435 -0
  276. package/src/api/apply.test.ts +238 -0
  277. package/src/api/apply.ts +384 -0
  278. package/src/api/bind.ts +58 -0
  279. package/src/api/client.ts +148 -0
  280. package/src/api/converge.ts +63 -0
  281. package/src/api/dashboards.ts +247 -0
  282. package/src/api/datasources.ts +33 -0
  283. package/src/api/fake-grafana-writes.ts +201 -0
  284. package/src/api/fake-grafana.ts +186 -0
  285. package/src/api/folders.ts +265 -0
  286. package/src/api/library-panels.ts +101 -0
  287. package/src/build.test.ts +210 -5
  288. package/src/build.ts +180 -43
  289. package/src/catalog.ts +86 -2
  290. package/src/codegen/docs.ts +42 -9
  291. package/src/codegen/schema-types.ts +16 -6
  292. package/src/composites/agent-dashboard.ts +6 -3
  293. package/src/composites/catalog.test.ts +2 -2
  294. package/src/composites/catalog.ts +82 -0
  295. package/src/composites/composites.test.ts +73 -9
  296. package/src/composites/index.ts +4 -2
  297. package/src/composites/queries.e2e.test.ts +374 -0
  298. package/src/composites/red-dashboard.ts +83 -12
  299. package/src/composites/shared.ts +11 -0
  300. package/src/composites/slo-alert-rules.ts +149 -0
  301. package/src/config.test.ts +61 -0
  302. package/src/config.ts +138 -0
  303. package/src/contact-point-secrets.ts +63 -0
  304. package/src/dashboard.ts +16 -2
  305. package/src/datasource-refs.test.ts +56 -0
  306. package/src/datasource-refs.ts +189 -0
  307. package/src/datasource.ts +52 -0
  308. package/src/deep-observe-hooks.ts +252 -0
  309. package/src/deep-observe.test.ts +265 -0
  310. package/src/deep-observe.ts +164 -0
  311. package/src/describe-resources.test.ts +255 -0
  312. package/src/describe-resources.ts +209 -0
  313. package/src/detect.ts +79 -2
  314. package/src/export-resources.test.ts +129 -0
  315. package/src/export-resources.ts +84 -0
  316. package/src/generated/lexicon-grafana.json +135 -0
  317. package/src/import/alerting-roundtrip.test.ts +161 -0
  318. package/src/import/alerting.ts +500 -0
  319. package/src/import/edits.ts +95 -0
  320. package/src/import/embedded.test.ts +45 -0
  321. package/src/import/embedded.ts +47 -0
  322. package/src/import/generated-types.e2e.test.ts +118 -0
  323. package/src/import/generator.test.ts +210 -0
  324. package/src/import/generator.ts +462 -0
  325. package/src/import/live-export.ts +264 -0
  326. package/src/import/mappings.ts +264 -0
  327. package/src/import/model.ts +110 -0
  328. package/src/import/normalize-alerting.ts +101 -0
  329. package/src/import/normalize.test.ts +117 -0
  330. package/src/import/normalize.ts +364 -0
  331. package/src/import/parser.test.ts +440 -0
  332. package/src/import/parser.ts +1236 -0
  333. package/src/import/report.ts +62 -0
  334. package/src/import/roundtrip.test.ts +468 -0
  335. package/src/import/testdata/fixtures.ts +129 -0
  336. package/src/import/v2.test.ts +292 -0
  337. package/src/import/v2.ts +803 -0
  338. package/src/import.e2e.test.ts +31 -84
  339. package/src/import.test.ts +9 -3
  340. package/src/index.ts +190 -5
  341. package/src/k8s.test.ts +98 -0
  342. package/src/k8s.ts +197 -0
  343. package/src/lint/audit-catalog.ts +60 -3
  344. package/src/lint/core-lint.test.ts +43 -0
  345. package/src/lint/post-synth/graf101.ts +3 -3
  346. package/src/lint/post-synth/graf102.ts +1 -1
  347. package/src/lint/post-synth/graf107.ts +1 -1
  348. package/src/lint/post-synth/graf108.ts +17 -0
  349. package/src/lint/post-synth/graf109.ts +17 -0
  350. package/src/lint/post-synth/graf111.ts +17 -0
  351. package/src/lint/post-synth/graf112.ts +17 -0
  352. package/src/lint/post-synth/graf113.ts +17 -0
  353. package/src/lint/post-synth/graf114.ts +17 -0
  354. package/src/lint/post-synth/graf115.ts +17 -0
  355. package/src/lint/post-synth/grafana-helpers.ts +15 -8
  356. package/src/lint/post-synth/index.ts +14 -0
  357. package/src/lint/post-synth/post-synth.test.ts +235 -11
  358. package/src/lint/rules/literal-secret.ts +60 -6
  359. package/src/lint/rules/rules.test.ts +27 -0
  360. package/src/lsp/completions.test.ts +5 -0
  361. package/src/lsp/hover.test.ts +7 -0
  362. package/src/lsp/hover.ts +8 -1
  363. package/src/members.ts +68 -0
  364. package/src/observe.e2e.test.ts +255 -0
  365. package/src/op/activities/grafana-apply.e2e.test.ts +175 -0
  366. package/src/op/activities/grafana-apply.test.ts +229 -0
  367. package/src/op/activities/grafana-apply.ts +171 -0
  368. package/src/op/activities/index.ts +8 -0
  369. package/src/op/builders.test.ts +40 -0
  370. package/src/op/builders.ts +34 -0
  371. package/src/ownership.ts +103 -0
  372. package/src/panel-options.ts +66 -0
  373. package/src/panels.test.ts +51 -0
  374. package/src/panels.ts +148 -10
  375. package/src/pin.ts +57 -6
  376. package/src/plugin.test.ts +5 -2
  377. package/src/plugin.ts +66 -0
  378. package/src/promql-check.test.ts +108 -0
  379. package/src/promql-check.ts +189 -0
  380. package/src/query.ts +13 -8
  381. package/src/schema/barchart.gen.ts +221 -0
  382. package/src/schema/bargauge.gen.ts +111 -0
  383. package/src/schema/canvas.gen.ts +176 -0
  384. package/src/schema/dashboard.gen.ts +206 -22
  385. package/src/schema/expr.gen.ts +618 -0
  386. package/src/schema/gauge.gen.ts +2 -1
  387. package/src/schema/geomap.gen.ts +112 -0
  388. package/src/schema/heatmap.gen.ts +2 -1
  389. package/src/schema/histogram.gen.ts +151 -0
  390. package/src/schema/index.ts +12 -0
  391. package/src/schema/logs.gen.ts +3 -1
  392. package/src/schema/loki.gen.ts +2 -1
  393. package/src/schema/nodegraph.gen.ts +58 -0
  394. package/src/schema/piechart.gen.ts +146 -0
  395. package/src/schema/prometheus.gen.ts +2 -1
  396. package/src/schema/stat.gen.ts +2 -1
  397. package/src/schema/statetimeline.gen.ts +143 -0
  398. package/src/schema/statushistory.gen.ts +132 -0
  399. package/src/schema/table.gen.ts +27 -4
  400. package/src/schema/tempo.gen.ts +2 -1
  401. package/src/schema/text.gen.ts +2 -1
  402. package/src/schema/timeseries.gen.ts +7 -2
  403. package/src/schema/trend.gen.ts +208 -0
  404. package/src/schema/xychart.gen.ts +167 -0
  405. package/src/schema-overlay.test.ts +282 -0
  406. package/src/schema-validate.ts +141 -50
  407. package/src/schema.test.ts +4 -2
  408. package/src/serializer.test.ts +13 -1
  409. package/src/serializer.ts +11 -3
  410. package/src/skill-defs.ts +17 -0
  411. package/src/skills/chant-grafana-alerting.md +49 -0
  412. package/src/skills/chant-grafana-provisioning.md +3 -3
  413. package/src/skills/chant-grafana.md +12 -6
  414. package/src/spec/fetch-cli.ts +3 -3
  415. package/src/spec/fetch-units-cli.ts +37 -0
  416. package/src/spec/overlay/canvas.overlay.json +142 -0
  417. package/src/spec/overlay/dashboard.overlay.json +378 -0
  418. package/src/spec/overlay/expr.overlay.json +14 -0
  419. package/src/spec/overlay/geomap.overlay.json +35 -0
  420. package/src/spec/overlay/logs.overlay.json +14 -0
  421. package/src/spec/overlay/nodegraph.overlay.json +31 -0
  422. package/src/spec/overlay/piechart.overlay.json +17 -0
  423. package/src/spec/overlay/table.overlay.json +84 -0
  424. package/src/spec/overlay/timeseries.overlay.json +21 -0
  425. package/src/spec/overlay/trend.overlay.json +17 -0
  426. package/src/spec/overlay/xychart.overlay.json +16 -0
  427. package/src/spec/overlay.ts +105 -0
  428. package/src/spec/schemas/barchart.jsonschema.json +419 -0
  429. package/src/spec/schemas/bargauge.jsonschema.json +224 -0
  430. package/src/spec/schemas/canvas.jsonschema.json +356 -0
  431. package/src/spec/schemas/dashboardv2.jsonschema.json +3387 -0
  432. package/src/spec/schemas/expr.jsonschema.json +987 -0
  433. package/src/spec/schemas/geomap.jsonschema.json +220 -0
  434. package/src/spec/schemas/histogram.jsonschema.json +308 -0
  435. package/src/spec/schemas/nodegraph.jsonschema.json +77 -0
  436. package/src/spec/schemas/piechart.jsonschema.json +275 -0
  437. package/src/spec/schemas/statetimeline.jsonschema.json +324 -0
  438. package/src/spec/schemas/statushistory.jsonschema.json +292 -0
  439. package/src/spec/schemas/trend.jsonschema.json +449 -0
  440. package/src/spec/schemas/xychart.jsonschema.json +428 -0
  441. package/src/spec/schemas.ts +22 -10
  442. package/src/spec/units.gen.ts +281 -0
  443. package/src/spec/units.ts +77 -0
  444. package/src/transformations.test.ts +65 -0
  445. package/src/transformations.ts +574 -0
  446. package/src/util.ts +8 -0
  447. package/src/validate-alerting.test.ts +255 -0
  448. package/src/validate-alerting.ts +428 -0
  449. package/src/validate-output.ts +332 -101
  450. package/src/validate.ts +14 -0
  451. package/src/variables.ts +7 -2
@@ -1,12 +1,15 @@
1
1
  import { describe, expect, test } from "vitest";
2
+ import { readdirSync, readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
2
5
  import { makePostSynthCtxFromFiles, makePostSynthCtx } from "@intentius/chant-test-utils";
3
6
  import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
4
7
  import type { Declarable } from "@intentius/chant/declarable";
5
8
  import { grafanaSerializer } from "../../serializer";
6
- import { Datasource } from "../../datasource";
7
- import { Dashboard } from "../../dashboard";
8
- import { Row, StatPanel, TimeSeriesPanel, TablePanel } from "../../panels";
9
- import { PromQuery, TempoQuery } from "../../query";
9
+ import { Datasource, ExternalDatasource } from "../../datasource";
10
+ import { Dashboard, DashboardProvider } from "../../dashboard";
11
+ import { HeatmapPanel, Row, StatPanel, TimeSeriesPanel, TablePanel, TextPanel } from "../../panels";
12
+ import { LokiQuery, PromQuery, TempoQuery } from "../../query";
10
13
  import { DatasourceVariable, QueryVariable } from "../../variables";
11
14
  import { graf101 } from "./graf101";
12
15
  import { graf102 } from "./graf102";
@@ -15,6 +18,14 @@ import { graf104 } from "./graf104";
15
18
  import { graf105 } from "./graf105";
16
19
  import { graf106 } from "./graf106";
17
20
  import { graf107 } from "./graf107";
21
+ import { graf108 } from "./graf108";
22
+ import { graf109 } from "./graf109";
23
+ import { graf115 } from "./graf115";
24
+ import { panelUnits } from "../../validate-output";
25
+ import { panelsOf } from "../../datasource-refs";
26
+ import { GRAFANA_UNIT_IDS } from "../../spec/units";
27
+ import { prometheusQueries } from "../../promql-check";
28
+ import { knownDatasources } from "../../datasource-refs";
18
29
 
19
30
  /** Serialize entities the way a build does and hand the files to the harness. */
20
31
  function ctxOf(entities: Record<string, Declarable>) {
@@ -72,10 +83,65 @@ describe("GRAF101: undeclared datasource", () => {
72
83
  expect(graf101.check(ctxOf({ prometheus, tempo, d }))).toEqual([]);
73
84
  });
74
85
 
75
- test("is silent when the build declares no datasource (another build root, chant #1939)", () => {
86
+ test("warns once per dashboard when the build declares no datasource, so nothing can be checked", () => {
76
87
  const panel = new StatPanel({ datasource: { type: "prometheus", uid: "elsewhere" }, targets: [up] });
88
+ const other = new StatPanel({ datasource: { type: "prometheus", uid: "elsewhere" }, targets: [up] });
89
+ const diags = graf101.check(ctxOf({ d: new Dashboard({ title: "D", panels: [panel, other] }) }));
90
+ expect(diags.map((d) => [d.checkId, d.severity])).toEqual([["GRAF101", "warning"]]);
91
+ expect(diags[0].message).toContain('datasource uid "elsewhere"');
92
+ expect(diags[0].message).toContain("ExternalDatasource");
93
+ });
94
+
95
+ test("says nothing when the build declares no datasource and the dashboard names none", () => {
96
+ const panel = new TextPanel({ options: { content: "hi", mode: "markdown" } });
77
97
  expect(graf101.check(ctxOf({ d: new Dashboard({ title: "D", panels: [panel] }) }))).toEqual([]);
78
98
  });
99
+
100
+ test("resolves entity and { type, uid } refs to an ExternalDatasource, and never provisions it", () => {
101
+ const mimir = new ExternalDatasource({ type: "prometheus", uid: "mimir", name: "Mimir" });
102
+ const d = new Dashboard({
103
+ title: "D",
104
+ panels: [new StatPanel({ datasource: mimir, targets: [up] }), new StatPanel({ datasource: { type: "prometheus", uid: "mimir" }, targets: [up] })],
105
+ variables: [new QueryVariable({ name: "job", datasource: mimir, query: "label_values(job)" })],
106
+ });
107
+ const entities = { mimir, prometheus, d };
108
+ expect(graf101.check(ctxOf(entities))).toEqual([]);
109
+ expect(graf102.check(ctxOf(entities))).toEqual([]);
110
+ const out = grafanaSerializer.serialize(new Map(Object.entries(entities))) as { primary: string; files: Record<string, string> };
111
+ expect(out.files["provisioning/datasources/chant.yaml"]).not.toContain("mimir");
112
+ expect(JSON.parse(out.primary).externalDatasources).toEqual([{ type: "prometheus", uid: "mimir", name: "Mimir" }]);
113
+ });
114
+
115
+ test("an ExternalDatasource alone is enough to check references", () => {
116
+ const mimir = new ExternalDatasource({ type: "prometheus", uid: "mimir" });
117
+ const d = new Dashboard({ title: "D", panels: [new StatPanel({ datasource: { type: "prometheus", uid: "typo" }, targets: [up] })] });
118
+ const diags = graf101.check(ctxOf({ mimir, d }));
119
+ expect(diags.map((x) => [x.checkId, x.severity])).toEqual([
120
+ ["GRAF101", "error"],
121
+ ["GRAF101", "error"],
122
+ ]);
123
+ expect(diags[0].message).toContain('Declared: "mimir"');
124
+ });
125
+
126
+ test("flags a DatasourceVariable whose plugin type no declared datasource has", () => {
127
+ const ds = new DatasourceVariable({ name: "logs", pluginType: "loki" });
128
+ const diags = graf101.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [ds] }) }));
129
+ expect(diags.map((x) => [x.checkId, x.severity])).toEqual([["GRAF101", "error"]]);
130
+ expect(diags[0].message).toContain('variable "logs" chooses among loki datasources');
131
+ });
132
+
133
+ test("passes a DatasourceVariable whose plugin type only an ExternalDatasource has", () => {
134
+ const ds = new DatasourceVariable({ name: "logs", pluginType: "loki" });
135
+ const loki = new ExternalDatasource({ type: "loki", uid: "loki-prod" });
136
+ expect(graf101.check(ctxOf({ prometheus, loki, d: new Dashboard({ title: "D", variables: [ds] }) }))).toEqual([]);
137
+ });
138
+
139
+ test("counts a DatasourceVariable in the no-datasource warning", () => {
140
+ const ds = new DatasourceVariable({ name: "logs", pluginType: "loki" });
141
+ const diags = graf101.check(ctxOf({ d: new Dashboard({ title: "D", variables: [ds] }) }));
142
+ expect(diags.map((x) => [x.checkId, x.severity])).toEqual([["GRAF101", "warning"]]);
143
+ expect(diags[0].message).toContain("datasource variables of type loki");
144
+ });
79
145
  });
80
146
 
81
147
  describe("GRAF102: datasource type mismatch", () => {
@@ -95,6 +161,13 @@ describe("GRAF102: datasource type mismatch", () => {
95
161
  expect(diags[0].message).toContain('variable "ds" chooses among loki');
96
162
  });
97
163
 
164
+ test("checks a ref's type against an ExternalDatasource", () => {
165
+ const traces = new ExternalDatasource({ type: "tempo", uid: "traces", name: "Traces" });
166
+ const panel = new StatPanel({ datasource: { type: "prometheus", uid: "traces" }, targets: [up] });
167
+ const diags = graf102.check(ctxOf({ traces, d: new Dashboard({ title: "D", panels: [panel] }) }));
168
+ expect(diags[0].message).toContain('external datasource "Traces", which is tempo');
169
+ });
170
+
98
171
  test("passes matching types", () => {
99
172
  expect(graf102.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", panels: [new StatPanel({ datasource: prometheus, targets: [up] })] }) }))).toEqual([]);
100
173
  });
@@ -180,6 +253,18 @@ describe("GRAF105: grid", () => {
180
253
  });
181
254
  });
182
255
 
256
+ describe("GRAF104 and GRAF106 on external datasources", () => {
257
+ test("flags an ExternalDatasource sharing a uid with a provisioned one", () => {
258
+ const clash = new ExternalDatasource({ type: "prometheus", uid: "prometheus" });
259
+ expect(graf104.check(ctxOf({ prometheus, clash })).map((d) => d.message)).toEqual([expect.stringContaining('2 datasources share the uid "prometheus"')]);
260
+ });
261
+
262
+ test("flags an ExternalDatasource uid Grafana rejects", () => {
263
+ const bad = new ExternalDatasource({ type: "prometheus", uid: "has space" });
264
+ expect(graf106.check(ctxOf({ bad })).map((d) => d.message)).toEqual([expect.stringContaining('ExternalDatasource "has space"')]);
265
+ });
266
+ });
267
+
183
268
  describe("GRAF106: uids and titles", () => {
184
269
  test("flags an over-long uid, a bad datasource uid and an empty title", () => {
185
270
  const bad = new Datasource({ name: "Bad", type: "prometheus", uid: "has space" });
@@ -197,17 +282,17 @@ describe("GRAF106: uids and titles", () => {
197
282
  });
198
283
 
199
284
  describe("GRAF107: the pinned schema", () => {
200
- test("flags an unknown dashboard key, a bad panel option and an unknown query field", () => {
285
+ test("warns about an unknown dashboard key and query field, and fails a bad panel option", () => {
201
286
  const dash = dashboardJson(
202
287
  [panelJson({ options: { graphMode: "sparkline" }, datasource: { type: "prometheus", uid: "p" }, targets: [{ refId: "A", expr: "up", exprr: "typo" }] })],
203
288
  [],
204
289
  { owner: "team-a" },
205
290
  );
206
- const messages = graf107.check(ctxOfJson(dash)).map((d) => d.message);
207
- expect(messages).toEqual([
208
- expect.stringContaining('must NOT have additional properties ("owner")'),
209
- expect.stringContaining("/panels/0/options/graphMode: must be equal to one of the allowed values"),
210
- expect.stringContaining('/panels/0/targets/0: must NOT have additional properties ("exprr")'),
291
+ const diags = graf107.check(ctxOfJson(dash)).map((d) => [d.severity, d.message]);
292
+ expect(diags).toEqual([
293
+ ["warning", expect.stringContaining('/: unknown key "owner" (not in the pinned schema)')],
294
+ ["error", expect.stringContaining("/panels/0/options/graphMode: must be equal to one of the allowed values")],
295
+ ["warning", expect.stringContaining('/panels/0/targets/0: unknown key "exprr" (not in the pinned schema)')],
211
296
  ]);
212
297
  });
213
298
 
@@ -229,3 +314,142 @@ describe("GRAF107: the pinned schema", () => {
229
314
  expect(graf107.check(ctx).map((d) => d.checkId)).toEqual(["GRAF107"]);
230
315
  });
231
316
  });
317
+
318
+ describe("GRAF108: PromQL syntax", () => {
319
+ const loki = new Datasource({ name: "Loki", type: "loki" });
320
+
321
+ test("flags the unbalanced expression from the issue (#2955)", () => {
322
+ const cluster = new QueryVariable({ name: "cluster", datasource: prometheus, query: "label_values(up, cluster)" });
323
+ const panel = new StatPanel({ title: "Up", datasource: prometheus, targets: [new PromQuery({ expr: 'sum(x{cluster="$cluster"}' })] });
324
+ const diags = graf108.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [cluster], panels: [panel] }) }));
325
+ expect(diags.map((d) => [d.checkId, d.severity])).toEqual([["GRAF108", "error"]]);
326
+ expect(diags[0].message).toContain('panel "Up"');
327
+ expect(diags[0].message).toContain("query A is not valid PromQL: the expression ends early");
328
+ });
329
+
330
+ test("flags a query variable and a panel query inheriting a Prometheus datasource variable", () => {
331
+ const ds = new DatasourceVariable({ name: "ds", pluginType: "prometheus" });
332
+ const job = new QueryVariable({ name: "job", datasource: prometheus, query: 'label_values(up{env="prod", job, instance)' });
333
+ const panel = new TimeSeriesPanel({ title: "Rate", datasource: ds, targets: [new PromQuery({ expr: "rate(http_requests_total[5 m])" })] });
334
+ const messages = graf108.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [ds, job], panels: [panel] }) })).map((d) => d.message);
335
+ expect(messages).toEqual([expect.stringContaining('panel "Rate" (id 1) query A is not valid PromQL'), expect.stringContaining('variable "job" query is not valid PromQL')]);
336
+ });
337
+
338
+ test("passes template variables and Grafana's macros where Prometheus would see a value", () => {
339
+ const route = new QueryVariable({ name: "route", datasource: prometheus, query: 'label_values(http_requests_total{job="$job"}, route)' });
340
+ const job = new QueryVariable({ name: "job", datasource: prometheus, query: "label_values(up, job)" });
341
+ const exprs = [
342
+ 'sum by (le) (rate(http_request_duration_seconds_bucket{route=~"$route"}[$__rate_interval]))',
343
+ 'sum(increase(http_requests_total{job="${job}"}[$__range])) / $__range_s',
344
+ 'rate(http_requests_total{job=~"[[job]]"}[$__interval]) offset $__interval',
345
+ ];
346
+ const panel = new TimeSeriesPanel({ datasource: prometheus, targets: exprs.map((expr) => new PromQuery({ expr })) });
347
+ expect(graf108.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [job, route], panels: [panel] }) }))).toEqual([]);
348
+ });
349
+
350
+ test("leaves LogQL, TraceQL and queries of unknown destination alone", () => {
351
+ const logs = new StatPanel({ datasource: loki, targets: [new LokiQuery({ expr: '{app="x"} |= "error" | json' })] });
352
+ const traces = new TablePanel({ datasource: tempo, targets: [new TempoQuery({ query: "{ .status = error }" })] });
353
+ const d = new Dashboard({ title: "D", panels: [logs, traces] });
354
+ expect(graf108.check(ctxOf({ loki, tempo, d }))).toEqual([]);
355
+ // No datasource anywhere: Grafana sends it to its default, whatever that is.
356
+ const unknown = dashboardJson([panelJson({ targets: [{ refId: "A", expr: "sum(" }] })]);
357
+ expect(graf108.check(ctxOfJson(unknown))).toEqual([]);
358
+ });
359
+
360
+ test("checks refs to a Prometheus the build does not declare, by the ref's own type", () => {
361
+ const dash = dashboardJson([panelJson({ datasource: { type: "prometheus", uid: "elsewhere" }, targets: [{ refId: "A", expr: "sum(up" }] })]);
362
+ expect(ids(graf108, ctxOfJson(dash))).toEqual([["GRAF108", "error"]]);
363
+ });
364
+ });
365
+
366
+ describe("GRAF108 over real Grafana exports", () => {
367
+ const exportsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "test", "fixtures", "exports");
368
+ const files = readdirSync(exportsDir)
369
+ .filter((d) => d.startsWith("grafana-"))
370
+ .flatMap((v) => readdirSync(join(exportsDir, v)).filter((f) => f.endsWith(".json") && !f.includes("-resource")).map((f) => `${v}/${f}`));
371
+
372
+ test.each(files)("%s has no GRAF108 errors", (name) => {
373
+ const text = readFileSync(join(exportsDir, name), "utf-8");
374
+ // Non-vacuous: every export in the corpus sends PromQL to a Prometheus.
375
+ expect(prometheusQueries(JSON.parse(text), knownDatasources([])).length).toBeGreaterThan(0);
376
+ expect(graf108.check(makePostSynthCtxFromFiles("grafana", { [name]: text }, "{}"))).toEqual([]);
377
+ });
378
+ });
379
+
380
+ describe("GRAF115: units", () => {
381
+ test("flags the units from the issue (#2954) and suggests the id meant", () => {
382
+ const bytes = new StatPanel({ title: "Memory", datasource: prometheus, targets: [up], fieldConfig: { defaults: { unit: "byte" } } });
383
+ const cores = new TimeSeriesPanel({ title: "CPU", datasource: prometheus, targets: [up], fieldConfig: { defaults: { unit: "cores" } } });
384
+ const diags = graf115.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", panels: [bytes, cores] }) }));
385
+ expect(diags.map((d) => [d.checkId, d.severity])).toEqual([
386
+ ["GRAF115", "warning"],
387
+ ["GRAF115", "warning"],
388
+ ]);
389
+ expect(diags[0].message).toContain('panel "Memory" (id 1) fieldConfig.defaults.unit: unit "byte" is not one Grafana knows');
390
+ expect(diags[0].message).toContain('Did you mean "bytes"?');
391
+ expect(diags[1].message).not.toContain("Did you mean");
392
+ expect(diags[1].message).toContain('"suffix:cores"');
393
+ });
394
+
395
+ test("passes registered ids, the legacy alias, an empty unit and every custom-unit syntax", () => {
396
+ const units = ["bytes", "s", "percentunit", "reqps", "currencyEUR", "farenheit", "", "suffix: cores", "prefix:$", "time:YYYY-MM-DD", "si:mF", "count:reqs", "currency:financial:€:suffix", "bool:up/down"];
397
+ const panels = units.map((unit) => new StatPanel({ datasource: prometheus, targets: [up], fieldConfig: { defaults: { unit } } }));
398
+ expect(graf115.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", panels }) }))).toEqual([]);
399
+ });
400
+
401
+ test("checks overrides, heatmap axes and cells, legacy y-axes and library panels", () => {
402
+ const heat = new HeatmapPanel({ title: "Heat", datasource: prometheus, targets: [up], options: { yAxis: { unit: "sec" }, cellValues: { unit: "Short" } } });
403
+ const table = new TablePanel({
404
+ title: "T",
405
+ datasource: prometheus,
406
+ targets: [up],
407
+ fieldConfig: { overrides: [{ matcher: { id: "byName", options: "x" }, properties: [{ id: "decimals", value: 2 }, { id: "unit", value: "celcius" }] }] },
408
+ });
409
+ const messages = graf115.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", panels: [heat, table] }) })).map((d) => d.message);
410
+ expect(messages).toEqual([
411
+ expect.stringContaining('options.yAxis.unit: unit "sec"'),
412
+ expect.stringContaining('options.cellValues.unit: unit "Short" is not one Grafana knows, so it is shown as a literal suffix. Did you mean "short"?'),
413
+ expect.stringContaining('fieldConfig.overrides[0].properties[1]: unit "celcius" is not one Grafana knows, so it is shown as a literal suffix. Did you mean "celsius"?'),
414
+ ]);
415
+
416
+ const graph = panelJson({ type: "graph", yaxes: [{ format: "bytes" }, { format: "ops/s" }] });
417
+ const lib = panelJson({ fieldConfig: { defaults: { unit: "req/s" }, overrides: [] } });
418
+ const dash = dashboardJson([graph], [], { __elements: { lib: { kind: 1, model: lib } } });
419
+ expect(graf115.check(ctxOfJson(dash)).map((d) => d.message)).toEqual([
420
+ expect.stringContaining('panel "p" (id 1) yaxes[1].format: unit "ops/s"'),
421
+ expect.stringContaining('library panel "lib" fieldConfig.defaults.unit: unit "req/s"'),
422
+ ]);
423
+ });
424
+
425
+ test("the vendored registry is Grafana v13.2.2's", () => {
426
+ expect(GRAFANA_UNIT_IDS.length).toBe(274);
427
+ expect(GRAFANA_UNIT_IDS).toEqual(expect.arrayContaining(["none", "bytes", "decbytes", "reqps", "ms", "percentunit", "celsius", "currencyUSD", "dateTimeAsIso", "h"]));
428
+ expect(GRAFANA_UNIT_IDS).not.toContain("byte");
429
+ });
430
+ });
431
+
432
+ describe("GRAF115 over real Grafana exports and community dashboards", () => {
433
+ const fixtures = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "test", "fixtures");
434
+ const exportsDir = join(fixtures, "exports");
435
+ const files = [
436
+ ...readdirSync(exportsDir)
437
+ .filter((d) => d.startsWith("grafana-"))
438
+ .flatMap((v) => readdirSync(join(exportsDir, v)).filter((f) => f.endsWith(".json") && !f.includes("-resource")).map((f) => `exports/${v}/${f}`)),
439
+ ...readdirSync(join(fixtures, "community"))
440
+ .filter((f) => f.endsWith(".json"))
441
+ .map((f) => `community/${f}`),
442
+ ];
443
+ let unitsSeen = 0;
444
+
445
+ test.each(files)("%s has no GRAF115 warnings", (name) => {
446
+ const text = readFileSync(join(fixtures, name), "utf-8");
447
+ unitsSeen += panelsOf(JSON.parse(text)).flatMap(({ panel }) => panelUnits(panel)).length;
448
+ expect(graf115.check(makePostSynthCtxFromFiles("grafana", { [name]: text }, "{}"))).toEqual([]);
449
+ });
450
+
451
+ // Non-vacuous: the corpus sets hundreds of units between them.
452
+ test("the corpus sets units", () => {
453
+ expect(unitsSeen).toBeGreaterThan(200);
454
+ });
455
+ });
@@ -1,15 +1,38 @@
1
1
  import * as ts from "typescript";
2
2
  import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
3
3
  import { constInitializers, constructorArgs, literalText, position, propertyName, resolveObject } from "./grafana-ast";
4
+ import { CONTACT_POINT_SECRET_SETTINGS, EXPANDED_VALUE as EXPANDED } from "../../contact-point-secrets";
4
5
 
5
- /** How Grafana provisioning reads a value from outside the file: `$VAR`, `${VAR}`, `$__env{…}`, `$__file{…}`, `$__vault{…}`. */
6
- const EXPANDED = /\$(\{[A-Za-z_][A-Za-z0-9_]*\}|[A-Za-z_][A-Za-z0-9_]*|__(env|file|vault)\{[^}]+\})/;
6
+ /** Follow an identifier to the array literal it was declared as, when it is one in this file. */
7
+ function resolveArray(node: ts.Expression, consts: Map<string, ts.Expression>, depth = 0): ts.ArrayLiteralExpression | undefined {
8
+ let n: ts.Expression = node;
9
+ while (ts.isAsExpression(n) || ts.isSatisfiesExpression(n) || ts.isParenthesizedExpression(n)) n = n.expression;
10
+ if (ts.isArrayLiteralExpression(n)) return n;
11
+ if (ts.isIdentifier(n) && depth < 5) {
12
+ const init = consts.get(n.text);
13
+ if (init) return resolveArray(init, consts, depth + 1);
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ /** The value of a property, `name: value` or the shorthand `name` (followed as an identifier). */
19
+ function prop(obj: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined {
20
+ for (const p of obj.properties) {
21
+ if (propertyName(p) === name) return (p as ts.PropertyAssignment).initializer;
22
+ if (ts.isShorthandPropertyAssignment(p) && p.name.text === name) return p.name;
23
+ }
24
+ return undefined;
25
+ }
7
26
 
8
27
  /**
9
- * GRAF002: a datasource secret written as a literal.
28
+ * GRAF002: a datasource or contact point secret written as a literal.
10
29
  *
11
- * `secureJsonData` holds passwords, tokens and TLS keys. A literal there is a
12
- * secret in the repository and in the emitted provisioning file. Grafana
30
+ * A datasource's `secureJsonData` holds passwords, tokens and TLS keys, and
31
+ * so do the settings Grafana stores encrypted for each contact point
32
+ * integration (a Slack webhook `url`, a PagerDuty `integrationKey`, a
33
+ * webhook's `authorization_credentials`; see `CONTACT_POINT_SECRET_SETTINGS`).
34
+ * A literal there is a secret in the repository and in the emitted
35
+ * provisioning file. Grafana
13
36
  * expands `$__env{NAME}`, `$__file{/path}` and `${NAME}` when it reads the
14
37
  * file, so the declaration never needs the value. A value written as a
15
38
  * named `const` in the same file is followed.
@@ -18,7 +41,7 @@ export const literalSecretRule: LintRule = {
18
41
  id: "GRAF002",
19
42
  severity: "error",
20
43
  category: "security",
21
- description: "Datasource secureJsonData value declared as a literal instead of $__env{...} or $__file{...}",
44
+ description: "Datasource secureJsonData or contact point secret setting declared as a literal instead of $__env{...} or $__file{...}",
22
45
 
23
46
  check(context: LintContext): LintDiagnostic[] {
24
47
  const diagnostics: LintDiagnostic[] = [];
@@ -43,6 +66,37 @@ export const literalSecretRule: LintRule = {
43
66
  }
44
67
  }
45
68
  }
69
+ for (const { arg } of constructorArgs(source, (n) => n === "ContactPoint")) {
70
+ const receivers = prop(arg, "receivers");
71
+ const list = receivers ? resolveArray(receivers, consts) : undefined;
72
+ for (const el of list?.elements ?? []) {
73
+ const receiver = resolveObject(el as ts.Expression, consts);
74
+ if (!receiver) continue;
75
+ const typeNode = prop(receiver, "type");
76
+ const type = typeNode ? literalText(typeNode) : undefined;
77
+ const settingsNode = prop(receiver, "settings");
78
+ const settings = settingsNode ? resolveObject(settingsNode, consts) : undefined;
79
+ if (!type || !settings) continue;
80
+ for (const path of CONTACT_POINT_SECRET_SETTINGS[type] ?? []) {
81
+ let node: ts.ObjectLiteralExpression | undefined = settings;
82
+ const segments = path.split(".");
83
+ for (const seg of segments.slice(0, -1)) {
84
+ const next: ts.Expression | undefined = node ? prop(node, seg) : undefined;
85
+ node = next ? resolveObject(next, consts) : undefined;
86
+ }
87
+ const valueNode = node ? prop(node, segments[segments.length - 1]) : undefined;
88
+ const text = valueNode ? literalText(valueNode) : undefined;
89
+ if (!valueNode || text === undefined || text === "" || EXPANDED.test(text)) continue;
90
+ diagnostics.push({
91
+ ruleId: "GRAF002",
92
+ severity: "error",
93
+ message: `${type} contact point setting ${path} is a literal secret; write "$__env{NAME}" or "$__file{/path}" and let Grafana read it`,
94
+ file: context.filePath,
95
+ ...position(source, valueNode),
96
+ });
97
+ }
98
+ }
99
+ }
46
100
  return diagnostics;
47
101
  },
48
102
  };
@@ -67,4 +67,31 @@ describe("GRAF002 literal secret", () => {
67
67
  );
68
68
  expect(diags).toEqual([]);
69
69
  });
70
+
71
+ test("flags a contact point's secret setting written as a literal, inline or through consts", () => {
72
+ const diags = literalSecretRule.check(
73
+ ctx(`
74
+ new ContactPoint({ name: "oncall", receivers: [{ type: "slack", settings: { url: "https://hooks.slack.com/services/T/B/X", recipient: "#c" } }] });
75
+ const pd = { integrationKey: "abc123" };
76
+ const receivers = [{ type: "pagerduty", settings: pd }, { type: "webhook", settings: { url: "https://x", tlsConfig: { clientKey: "-----BEGIN" } } }];
77
+ new ContactPoint({ name: "pager", receivers });
78
+ `),
79
+ );
80
+ expect(diags.map((d) => d.message)).toEqual([
81
+ expect.stringContaining("slack contact point setting url"),
82
+ expect.stringContaining("pagerduty contact point setting integrationKey"),
83
+ expect.stringContaining("webhook contact point setting tlsConfig.clientKey"),
84
+ ]);
85
+ });
86
+
87
+ test("accepts expanded contact point secrets, and settings that are not secret", () => {
88
+ const diags = literalSecretRule.check(
89
+ ctx(`
90
+ new ContactPoint({ name: "oncall", receivers: [{ type: "slack", settings: { url: "$__env{SLACK_URL}", recipient: "#c" } }] });
91
+ new ContactPoint({ name: "hook", receivers: [{ type: "webhook", settings: { url: "https://tickets.example.com", authorization_credentials: "\${TOKEN}" } }] });
92
+ new ContactPoint({ name: "mail", receivers: [{ type: "email", settings: { addresses: "a@example.com" } }] });
93
+ `),
94
+ );
95
+ expect(diags).toEqual([]);
96
+ });
70
97
  });
@@ -14,6 +14,11 @@ describe("grafana LSP completions", () => {
14
14
  expect(items.map((i) => i.label)).toContain("TimeSeriesPanel");
15
15
  });
16
16
 
17
+ it("offers every panel class sharing a prefix", () => {
18
+ const items = completions({ uri: "file:///d.ts", content: "new Sta", position: { line: 0, character: 7 }, wordAtCursor: "Sta", linePrefix: "new Sta" });
19
+ expect(items.map((i) => i.label)).toEqual(expect.arrayContaining(["StatPanel", "StateTimelinePanel", "StatusHistoryPanel"]));
20
+ });
21
+
17
22
  it("offers the query classes by prefix", () => {
18
23
  const items = completions({
19
24
  uri: "file:///q.ts",
@@ -14,6 +14,13 @@ describe("grafana LSP hover", () => {
14
14
  expect(info?.contents).toContain("Panel plugin `stat`");
15
15
  });
16
16
 
17
+ it("names the schema a panel's options come from, or that there is none", () => {
18
+ const at = (word: string) => hover({ uri: "file:///d.ts", content: `new ${word}({})`, position: { line: 0, character: 6 }, word, lineText: `new ${word}({})` })?.contents;
19
+ expect(at("NodeGraphPanel")).toContain("Panel plugin `nodeGraph`. Options typed from");
20
+ expect(at("NodeGraphPanel")).toContain("(`nodegraph`)");
21
+ expect(at("AlertListPanel")).toContain("Grafana publishes no options schema for it");
22
+ });
23
+
17
24
  it("describes a query and its datasource type", () => {
18
25
  const info = hover({ uri: "file:///q.ts", content: "new TempoQuery({})", position: { line: 0, character: 6 }, word: "TempoQuery", lineText: "new TempoQuery({})" });
19
26
  expect(info?.contents).toContain("`tempo` datasources");
package/src/lsp/hover.ts CHANGED
@@ -2,6 +2,7 @@ import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
2
2
  import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
3
3
  import { BUILTIN_CATALOG, lexiconRegistry } from "../catalog";
4
4
  import { GRAFANA_SCHEMA_PIN } from "../pin";
5
+ import { panelDefinitionFor } from "../panels";
5
6
 
6
7
  let cachedIndex: LexiconIndex | null = null;
7
8
 
@@ -20,7 +21,13 @@ function resourceHover(className: string, entry: LexiconEntry): HoverInfo | unde
20
21
  const lines = [`**${className}**`, "", `grafana type: \`${entry.resourceType}\``];
21
22
  if (cat?.description) lines.push("", cat.description);
22
23
  if (cat?.kind === "panel") {
23
- lines.push("", `Panel plugin \`${cat.pluginId}\`. Options typed from ${GRAFANA_SCHEMA_PIN.source} ${GRAFANA_SCHEMA_PIN.ref}.`);
24
+ const schema = cat.pluginId ? panelDefinitionFor(cat.pluginId)?.schema : undefined;
25
+ lines.push(
26
+ "",
27
+ schema
28
+ ? `Panel plugin \`${cat.pluginId}\`. Options typed from ${GRAFANA_SCHEMA_PIN.source} ${GRAFANA_SCHEMA_PIN.ref} (\`${schema}\`).`
29
+ : `Panel plugin \`${cat.pluginId}\`. Grafana publishes no options schema for it, so GRAF107 does not check its options.`,
30
+ );
24
31
  } else if (cat?.kind === "query") {
25
32
  lines.push("", `For \`${cat.pluginId}\` datasources. Typed from ${GRAFANA_SCHEMA_PIN.source} ${GRAFANA_SCHEMA_PIN.ref}.`);
26
33
  }
package/src/members.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Which dashboard holds a declared panel, row, query or variable (#2946).
3
+ *
4
+ * Panels, rows, queries and variables are property-kind declarables: they
5
+ * exist in Grafana only inside a dashboard's JSON. Core's live diff still
6
+ * hands every exported declarable to `describeResources`, so a project that
7
+ * exports its panels (as every example and every imported dashboard does)
8
+ * asks the reader about each one. Answering "absent" would make `lifecycle
9
+ * plan` propose creating it, and "not observed" would report a hole for every
10
+ * panel of a dashboard that was read in full. The honest answer is the
11
+ * dashboard's own verdict: a panel is there when the dashboard holding it is
12
+ * there, and its properties are compared as part of that dashboard's tree.
13
+ *
14
+ * Membership is by identity: a dashboard's props hold the very declarable
15
+ * objects, so the member is found by walking them, whatever nests it (a row's
16
+ * panels, a panel's queries, a `repeat` or `datasource` variable).
17
+ */
18
+
19
+ import { DASHBOARD_TYPE } from "./dashboard";
20
+ import { PANEL_TYPE_PREFIX, ROW_TYPE } from "./panels";
21
+ import { QUERY_TYPE_PREFIX } from "./query";
22
+ import { VARIABLE_TYPE_PREFIX } from "./variables";
23
+
24
+ type Entities = Map<string, { entityType: string; props: Record<string, unknown> }>;
25
+
26
+ function isPropertyDeclarable(v: unknown): v is { kind: "property"; props: Record<string, unknown> } {
27
+ return typeof v === "object" && v !== null && (v as { kind?: unknown }).kind === "property" && typeof (v as { props?: unknown }).props === "object";
28
+ }
29
+
30
+ function collect(value: unknown, into: Set<object>, seen: Set<object>): void {
31
+ if (typeof value !== "object" || value === null || seen.has(value)) return;
32
+ seen.add(value);
33
+ if (isPropertyDeclarable(value)) {
34
+ into.add(value.props);
35
+ collect(value.props, into, seen);
36
+ return;
37
+ }
38
+ if (Array.isArray(value)) {
39
+ for (const v of value) collect(v, into, seen);
40
+ return;
41
+ }
42
+ if (Object.getPrototypeOf(value) === Object.prototype) {
43
+ for (const v of Object.values(value)) collect(v, into, seen);
44
+ }
45
+ }
46
+
47
+ /** Member entity name -> the name of the first declared dashboard holding it. Dashboards and datasources are not members. */
48
+ export function dashboardMembers(entities: Entities): Map<string, string> {
49
+ const held = new Map<object, string>();
50
+ for (const [name, e] of entities) {
51
+ if (e.entityType !== DASHBOARD_TYPE) continue;
52
+ const props = new Set<object>();
53
+ collect(e.props, props, new Set());
54
+ for (const p of props) if (!held.has(p)) held.set(p, name);
55
+ }
56
+ const out = new Map<string, string>();
57
+ for (const [name, e] of entities) {
58
+ if (e.entityType === DASHBOARD_TYPE) continue;
59
+ const dashboard = held.get(e.props);
60
+ if (dashboard !== undefined) out.set(name, dashboard);
61
+ }
62
+ return out;
63
+ }
64
+
65
+ /** Entity types that only ever live inside a dashboard. */
66
+ export function isDashboardPart(entityType: string): boolean {
67
+ return entityType === ROW_TYPE || [PANEL_TYPE_PREFIX, QUERY_TYPE_PREFIX, VARIABLE_TYPE_PREFIX].some((p) => entityType.startsWith(p));
68
+ }