@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
@@ -0,0 +1,500 @@
1
+ /**
2
+ * An alerting provisioning file -> a `Plan`, for `chant import`.
3
+ *
4
+ * Accepts what Grafana's file provisioner reads and what
5
+ * `GET /api/v1/provisioning/*\/export` writes (the same format at v12.4.11
6
+ * and v13.2.2): rule groups, contact points, notification policies, mute
7
+ * timings and templates, in one file or split across several.
8
+ *
9
+ * - A rule group becomes an `AlertRuleGroup`, each rule an `AlertRule`,
10
+ * each entry of a rule's `data` a declaration of its own: a server-side
11
+ * expression as its typed class (`ReduceExpression`, `ThresholdExpression`,
12
+ * ...), and a datasource query as an `AlertQuery` holding its model as it
13
+ * is, so nothing a plugin stores is lost.
14
+ * - A datasource uid a query names becomes an `ExternalDatasource` when the
15
+ * file says its plugin type (`model.datasource.type`), so the checks can
16
+ * see it (GRAF108 parses Prometheus queries). When the type has to be
17
+ * inferred from the model, the import says so; when it cannot be told,
18
+ * the query keeps the bare uid and the import says that too.
19
+ * - A contact point, policy, mute timing or template becomes its class;
20
+ * a receiver or timing named in the same file is referenced by variable.
21
+ * - `deleteRules`, `deleteContactPoints`, `resetPolicies`, `deleteMuteTimes`
22
+ * and `deleteTemplates` are not carried, with a warning.
23
+ *
24
+ * Grafana's editor writes keys into expression models that the expression
25
+ * does not read (a `conditions` block on reduce and math, `operator`,
26
+ * `query`, `reducer` and `type` in a threshold's conditions, `type` and
27
+ * `reducer.params` in classic conditions). They are left out as edits without a warning:
28
+ * Grafana evaluates the rule the same without them.
29
+ */
30
+
31
+ import { pointer, type ImportEdit } from "./edits";
32
+ import { NO_PROP, Report } from "./report";
33
+ import { declRef, type Declaration, type DeclRef, type Plan } from "./model";
34
+ import { isObject, refIdAt } from "./normalize";
35
+ import { DEFAULT_RELATIVE_TIME_RANGE } from "../alerting-build";
36
+ import { EXPRESSION_DATASOURCE_UID, type ExpressionKind } from "../alerting";
37
+ import { validateExpressionSchema } from "../schema-validate";
38
+ import { REDACTED, secretSettings } from "../contact-point-secrets";
39
+
40
+ type Json = Record<string, unknown>;
41
+
42
+ const EXPRESSION_CLASS: Record<ExpressionKind, string> = {
43
+ reduce: "ReduceExpression",
44
+ math: "MathExpression",
45
+ threshold: "ThresholdExpression",
46
+ resample: "ResampleExpression",
47
+ classic_conditions: "ClassicConditionsExpression",
48
+ sql: "SqlExpression",
49
+ };
50
+
51
+ /** Keys every expression model may hold (the pinned `expr` schema's common fields). */
52
+ const EXPRESSION_COMMON = ["hide", "intervalMs", "maxDataPoints", "queryType", "resultAssertions", "timeRange"];
53
+
54
+ /** Keys each expression type reads, from the pinned `expr` schema. */
55
+ const EXPRESSION_KEYS: Record<ExpressionKind, string[]> = {
56
+ math: ["expression"],
57
+ reduce: ["expression", "reducer", "settings"],
58
+ resample: ["expression", "window", "downsampler", "upsampler"],
59
+ classic_conditions: ["conditions"],
60
+ threshold: ["expression", "conditions"],
61
+ sql: ["expression", "format"],
62
+ };
63
+
64
+ /**
65
+ * Keys of a condition each command reads: the threshold command
66
+ * (`pkg/expr/threshold.go`) and classic conditions (`ConditionJSON` in
67
+ * `pkg/expr/classic`). The editor writes more (`operator`, `query` and
68
+ * `reducer` on a threshold, `type: query` on both).
69
+ */
70
+ const CONDITION_KEYS: Partial<Record<ExpressionKind, string[]>> = {
71
+ threshold: ["evaluator", "unloadEvaluator", "loadedDimensions", "loadedFingerprints"],
72
+ classic_conditions: ["evaluator", "operator", "query", "reducer"],
73
+ };
74
+
75
+ /** A classic condition's reducer is read by `type` alone; the editor also writes an empty `params` (`ConditionReducerJSON`). */
76
+ function classicReducerParams(kind: ExpressionKind, c: Json): boolean {
77
+ return kind === "classic_conditions" && isObject(c.reducer) && "params" in c.reducer;
78
+ }
79
+
80
+ function conditionsKept(kind: ExpressionKind, conditions: unknown[]): unknown[] {
81
+ const keys = CONDITION_KEYS[kind]!;
82
+ return conditions.map((c) => {
83
+ if (!isObject(c)) return c;
84
+ const kept = Object.fromEntries(Object.entries(c).filter(([k]) => keys.includes(k)));
85
+ if (classicReducerParams(kind, c)) {
86
+ const reducer = { ...(c.reducer as Json) };
87
+ delete reducer.params;
88
+ kept.reducer = reducer;
89
+ }
90
+ return kept;
91
+ });
92
+ }
93
+
94
+ const RULE_FIELDS = [
95
+ "uid",
96
+ "title",
97
+ "condition",
98
+ "data",
99
+ "dashboardUid",
100
+ "dasboardUid",
101
+ "panelId",
102
+ "noDataState",
103
+ "execErrState",
104
+ "for",
105
+ "keepFiringFor",
106
+ "missing_series_evals_to_resolve",
107
+ "annotations",
108
+ "labels",
109
+ "isPaused",
110
+ "notification_settings",
111
+ "record",
112
+ ];
113
+
114
+ const GROUP_FIELDS = ["orgId", "name", "folder", "interval", "rules"];
115
+ const NOTIFICATION_SETTINGS_FIELDS = ["receiver", "group_by", "group_wait", "group_interval", "repeat_interval", "mute_time_intervals", "active_time_intervals"];
116
+ const ROUTE_FIELDS = [
117
+ "receiver",
118
+ "group_by",
119
+ "object_matchers",
120
+ "matchers",
121
+ "match",
122
+ "match_re",
123
+ "continue",
124
+ "group_wait",
125
+ "group_interval",
126
+ "repeat_interval",
127
+ "mute_time_intervals",
128
+ "active_time_intervals",
129
+ "routes",
130
+ ];
131
+ const DELETIONS: Record<string, string> = {
132
+ deleteRules: "rule deletions",
133
+ deleteContactPoints: "contact point deletions",
134
+ resetPolicies: "policy resets",
135
+ deleteMuteTimes: "mute timing deletions",
136
+ deleteTemplates: "template deletions",
137
+ };
138
+
139
+ function isExpressionKind(v: unknown): v is ExpressionKind {
140
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(EXPRESSION_CLASS, v);
141
+ }
142
+
143
+ /** A datasource's plugin type as far as one query shows it: stated in the model, or inferred from its keys. */
144
+ function pluginTypeOf(model: Json, queryType: unknown): { type: string; inferred: boolean } | undefined {
145
+ const ds = model.datasource;
146
+ if (isObject(ds) && typeof ds.type === "string" && ds.type !== "" && ds.type !== EXPRESSION_DATASOURCE_UID) return { type: ds.type, inferred: false };
147
+ // Prometheus: PromQL in `expr` with the editor's `instant`/`range` switches, and no Loki `queryType`.
148
+ if (typeof model.expr === "string" && (typeof model.instant === "boolean" || typeof model.range === "boolean") && model.queryType === undefined && !queryType) {
149
+ return { type: "prometheus", inferred: true };
150
+ }
151
+ return undefined;
152
+ }
153
+
154
+ class AlertingConverter {
155
+ readonly declarations: Declaration[] = [];
156
+ private readonly datasources = new Map<string, { id: string; type: string; inferred: boolean }>();
157
+ private readonly untyped = new Set<string>();
158
+ private readonly contactPoints = new Map<string, string>();
159
+ private readonly timings = new Map<string, string>();
160
+
161
+ constructor(
162
+ private readonly doc: Json,
163
+ private readonly report: Report,
164
+ ) {}
165
+
166
+ private receiverRef(name: unknown): unknown {
167
+ return typeof name === "string" && this.contactPoints.has(name) ? declRef(this.contactPoints.get(name)!) : name;
168
+ }
169
+
170
+ private timingRefs(list: unknown): unknown {
171
+ return Array.isArray(list) ? list.map((n) => (typeof n === "string" && this.timings.has(n) ? declRef(this.timings.get(n)!) : n)) : list;
172
+ }
173
+
174
+ /** Scan every query for the datasources it names, and declare the ones whose type is known. */
175
+ private scanDatasources(groups: unknown[]): void {
176
+ const found = new Map<string, { type?: string; inferred: boolean }>();
177
+ for (const g of groups) {
178
+ for (const r of isObject(g) && Array.isArray(g.rules) ? g.rules : []) {
179
+ for (const q of isObject(r) && Array.isArray(r.data) ? r.data : []) {
180
+ if (!isObject(q) || typeof q.datasourceUid !== "string" || q.datasourceUid === EXPRESSION_DATASOURCE_UID) continue;
181
+ const t = pluginTypeOf(isObject(q.model) ? q.model : {}, q.queryType);
182
+ const prev = found.get(q.datasourceUid);
183
+ if (!prev?.type || (prev.inferred && t && !t.inferred)) found.set(q.datasourceUid, { type: t?.type, inferred: t?.inferred ?? false });
184
+ }
185
+ }
186
+ }
187
+ for (const [uid, t] of [...found].sort(([a], [b]) => a.localeCompare(b))) {
188
+ if (!t.type) {
189
+ this.untyped.add(uid);
190
+ continue;
191
+ }
192
+ const id = `datasource:${uid}`;
193
+ this.datasources.set(uid, { id, type: t.type, inferred: t.inferred });
194
+ this.declarations.push({ id, kind: "new", className: "ExternalDatasource", props: { type: t.type, uid }, name: uid, module: "datasources" });
195
+ }
196
+ const inferred = [...this.datasources].filter(([, d]) => d.inferred).map(([uid]) => `"${uid}"`);
197
+ if (inferred.length > 0) {
198
+ this.report.warn(
199
+ `datasources: the file does not say the plugin type of ${inferred.join(", ")}; the queries look like PromQL, so ${inferred.length === 1 ? "it is" : "they are"} declared as a prometheus ExternalDatasource. Change the type if that is wrong.`,
200
+ );
201
+ }
202
+ if (this.untyped.size > 0) {
203
+ const uids = [...this.untyped].map((u) => `"${u}"`).join(", ");
204
+ this.report.warn(
205
+ `datasources: the file does not say the plugin type of ${uids}, so the queries name ${this.untyped.size === 1 ? "it" : "them"} by uid alone and the checks cannot see what ${this.untyped.size === 1 ? "it is" : "they are"}. Declare an ExternalDatasource with its type and pass it as the query's datasource.`,
206
+ );
207
+ }
208
+ }
209
+
210
+ private templates(list: unknown[]): void {
211
+ list.forEach((t, i) => {
212
+ if (!isObject(t) || typeof t.name !== "string" || typeof t.template !== "string") {
213
+ this.report.drop(pointer("templates", i), "templates", `entry ${i}`, "(it has no name or template)");
214
+ return;
215
+ }
216
+ const props: Json = { name: t.name, ...(t.orgId !== undefined ? { orgId: t.orgId } : {}), template: t.template };
217
+ for (const k of Object.keys(t)) if (!["name", "orgId", "template"].includes(k)) this.report.drop(pointer("templates", i, k), `template "${t.name}"`, k, NO_PROP);
218
+ this.declarations.push({ id: `template:${i}`, kind: "new", className: "NotificationTemplate", props, name: `${t.name} template`, module: "notifications" });
219
+ });
220
+ }
221
+
222
+ private muteTimes(list: unknown[]): void {
223
+ list.forEach((t, i) => {
224
+ if (!isObject(t) || typeof t.name !== "string") {
225
+ this.report.drop(pointer("muteTimes", i), "muteTimes", `entry ${i}`, "(it has no name)");
226
+ return;
227
+ }
228
+ const id = `mute:${i}`;
229
+ this.timings.set(t.name, id);
230
+ const props: Json = { name: t.name, ...(t.orgId !== undefined ? { orgId: t.orgId } : {}), time_intervals: Array.isArray(t.time_intervals) ? t.time_intervals : [] };
231
+ for (const k of Object.keys(t)) if (!["name", "orgId", "time_intervals"].includes(k)) this.report.drop(pointer("muteTimes", i, k), `mute timing "${t.name}"`, k, NO_PROP);
232
+ this.declarations.push({ id, kind: "new", className: "MuteTiming", props, name: `${t.name} mute timing`, module: "notifications" });
233
+ });
234
+ }
235
+
236
+ private contactPointList(list: unknown[]): void {
237
+ list.forEach((cp, i) => {
238
+ if (!isObject(cp) || typeof cp.name !== "string") {
239
+ this.report.drop(pointer("contactPoints", i), "contactPoints", `entry ${i}`, "(it has no name)");
240
+ return;
241
+ }
242
+ const id = `contact-point:${i}`;
243
+ this.contactPoints.set(cp.name, id);
244
+ const subject = `contact point "${cp.name}"`;
245
+ const receivers = (Array.isArray(cp.receivers) ? cp.receivers : []).flatMap((r, j) => {
246
+ if (!isObject(r) || typeof r.type !== "string") {
247
+ this.report.drop(pointer("contactPoints", i, "receivers", j), subject, `receivers[${j}]`, "(it has no type)");
248
+ return [];
249
+ }
250
+ const out: Json = {};
251
+ for (const k of ["uid", "type", "settings", "disableResolveMessage"]) if (r[k] !== undefined && r[k] !== null) out[k] = r[k];
252
+ if (out.settings === undefined) out.settings = {};
253
+ if (isObject(out.settings)) out.settings = this.redacted(out.settings, r.type as string, cp.name as string, ["contactPoints", i, "receivers", j, "settings"]);
254
+ for (const k of Object.keys(r)) {
255
+ if (!["uid", "type", "settings", "disableResolveMessage"].includes(k)) this.report.drop(pointer("contactPoints", i, "receivers", j, k), subject, k, NO_PROP);
256
+ }
257
+ return [out];
258
+ });
259
+ for (const k of Object.keys(cp)) if (!["name", "orgId", "receivers"].includes(k)) this.report.drop(pointer("contactPoints", i, k), subject, k, NO_PROP);
260
+ const props: Json = { name: cp.name, ...(cp.orgId !== undefined ? { orgId: cp.orgId } : {}), receivers };
261
+ this.declarations.push({ id, kind: "new", className: "ContactPoint", props, name: `${cp.name} contact point`, module: "notifications" });
262
+ });
263
+ }
264
+
265
+ /**
266
+ * An export made without secrets writes `[REDACTED]` for each; provisioning
267
+ * that would send the literal text. Each becomes `$__env{NAME}`, named from
268
+ * the contact point, integration and setting, with a warning.
269
+ */
270
+ private redacted(settings: Json, type: string, name: string, path: Array<string | number>): Json {
271
+ const out = structuredClone(settings);
272
+ const names: string[] = [];
273
+ for (const [key, value] of secretSettings(type, settings)) {
274
+ if (value !== REDACTED) continue;
275
+ const env = `${name} ${type} ${key}`.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_|_$/g, "");
276
+ const segments = key.split(".");
277
+ let node = out;
278
+ for (const seg of segments.slice(0, -1)) node = node[seg] as Json;
279
+ node[segments[segments.length - 1]] = `$__env{${env}}`;
280
+ this.report.edit({ op: "replace", path: pointer(...path, ...segments), value: `$__env{${env}}` });
281
+ names.push(`${key} as $__env{${env}}`);
282
+ }
283
+ if (names.length > 0) {
284
+ this.report.warn(`contact point "${name}" (${type}): the file was exported without secrets, so ${names.join(" and ")} ${names.length === 1 ? "is" : "are"} read from the environment; set ${names.length === 1 ? "it" : "them"} where Grafana runs`);
285
+ }
286
+ return out;
287
+ }
288
+
289
+ private route(r: Json, path: Array<string | number>, subject: string): Json {
290
+ const out: Json = {};
291
+ for (const k of ROUTE_FIELDS) {
292
+ const v = r[k];
293
+ if (v === undefined || v === null) continue;
294
+ if (k === "receiver") out.receiver = this.receiverRef(v);
295
+ else if (k === "mute_time_intervals" || k === "active_time_intervals") out[k] = this.timingRefs(v);
296
+ else if (k === "routes" && Array.isArray(v)) out.routes = v.map((c, i) => (isObject(c) ? this.route(c, [...path, "routes", i], subject) : c));
297
+ else out[k] = v;
298
+ }
299
+ for (const k of Object.keys(r)) if (!ROUTE_FIELDS.includes(k) && k !== "orgId") this.report.drop(pointer(...path, k), subject, k, NO_PROP);
300
+ return out;
301
+ }
302
+
303
+ private policies(list: unknown[]): void {
304
+ list.forEach((p, i) => {
305
+ if (!isObject(p) || typeof p.receiver !== "string") {
306
+ this.report.drop(pointer("policies", i), "policies", `entry ${i}`, "(the root policy has no receiver)");
307
+ return;
308
+ }
309
+ const subject = "the notification policy tree";
310
+ const route = this.route(p, ["policies", i], subject);
311
+ for (const k of ["object_matchers", "matchers", "match", "match_re", "continue"]) {
312
+ if (route[k] !== undefined) {
313
+ this.report.drop(pointer("policies", i, k), subject, k, "(the root policy matches every alert)");
314
+ delete route[k];
315
+ }
316
+ }
317
+ const props: Json = { ...(p.orgId !== undefined ? { orgId: p.orgId } : {}), ...route };
318
+ const org = typeof p.orgId === "number" && p.orgId !== 1 ? ` org ${p.orgId}` : "";
319
+ this.declarations.push({ id: `policy:${i}`, kind: "new", className: "NotificationPolicy", props, name: `notification policy${org}`, module: "notifications" });
320
+ });
321
+ }
322
+
323
+ /** True when the model, less the keys the editor writes and the expression ignores, is valid for its typed class. */
324
+ private fitsSchema(model: Json, kind: ExpressionKind, vestigial: string[]): boolean {
325
+ const m: Json = { ...model };
326
+ for (const k of [...vestigial, "datasource", "refId"]) delete m[k];
327
+ if (CONDITION_KEYS[kind] && Array.isArray(m.conditions)) m.conditions = conditionsKept(kind, m.conditions);
328
+ return validateExpressionSchema(m).length === 0;
329
+ }
330
+
331
+ /** One entry of a rule's `data`: its declaration's class and props. */
332
+ private query(q: Json, path: Array<string | number>, index: number, subject: string): { className: string; props: Json } {
333
+ const model = isObject(q.model) ? q.model : {};
334
+ const refId = typeof q.refId === "string" ? q.refId : refIdAt(index);
335
+ const range = isObject(q.relativeTimeRange) ? q.relativeTimeRange : undefined;
336
+ for (const k of Object.keys(q)) {
337
+ if (!["refId", "queryType", "relativeTimeRange", "datasourceUid", "model"].includes(k)) this.report.drop(pointer(...path, k), subject, k, NO_PROP);
338
+ }
339
+
340
+ if (q.datasourceUid === EXPRESSION_DATASOURCE_UID && isExpressionKind(model.type) && (q.queryType === undefined || q.queryType === "")) {
341
+ const kind = model.type;
342
+ const allowed = new Set([...EXPRESSION_COMMON, ...EXPRESSION_KEYS[kind], "type", "datasource", "refId"]);
343
+ const vestigial = kind !== "classic_conditions" && kind !== "threshold" ? ["conditions"] : [];
344
+ const unknown = Object.keys(model).filter((k) => !allowed.has(k) && !vestigial.includes(k));
345
+ const ds = model.datasource;
346
+ const dsIsExpr = ds === undefined || (isObject(ds) && ds.uid === EXPRESSION_DATASOURCE_UID && Object.keys(ds).every((k) => k === "type" || k === "uid"));
347
+ if (unknown.length === 0 && dsIsExpr && this.fitsSchema(model, kind, vestigial)) {
348
+ const props: Json = {};
349
+ if (refId !== refIdAt(index)) props.refId = refId;
350
+ if (range && (range.from !== 0 || (range.to !== undefined && range.to !== 0))) props.relativeTimeRange = range;
351
+ for (const [k, v] of Object.entries(model)) {
352
+ if (k === "type" || k === "datasource" || k === "refId") continue;
353
+ if (vestigial.includes(k)) {
354
+ this.report.drop(pointer(...path, "model", k), subject, k);
355
+ continue;
356
+ }
357
+ if (k === "conditions" && CONDITION_KEYS[kind] && Array.isArray(v)) {
358
+ v.forEach((c, ci) => {
359
+ if (!isObject(c)) return;
360
+ for (const ck of Object.keys(c)) if (!CONDITION_KEYS[kind]!.includes(ck)) this.report.drop(pointer(...path, "model", "conditions", ci, ck), subject, ck);
361
+ if (classicReducerParams(kind, c)) this.report.drop(pointer(...path, "model", "conditions", ci, "reducer", "params"), subject, "reducer.params");
362
+ });
363
+ props.conditions = conditionsKept(kind, v);
364
+ continue;
365
+ }
366
+ props[k] = v;
367
+ }
368
+ return { className: EXPRESSION_CLASS[kind], props };
369
+ }
370
+ }
371
+
372
+ // A datasource query, or an expression the typed classes cannot hold: its model as it is.
373
+ const uid = typeof q.datasourceUid === "string" ? q.datasourceUid : undefined;
374
+ const props: Json = {};
375
+ if (uid !== undefined) props.datasource = this.datasources.has(uid) ? declRef(this.datasources.get(uid)!.id) : uid;
376
+ props.model = model;
377
+ if (refId !== refIdAt(index)) props.refId = refId;
378
+ if (typeof q.queryType === "string" && q.queryType !== "") props.queryType = q.queryType;
379
+ const from = range?.from ?? 0;
380
+ const to = range?.to ?? 0;
381
+ if (from !== DEFAULT_RELATIVE_TIME_RANGE.from || to !== DEFAULT_RELATIVE_TIME_RANGE.to) props.relativeTimeRange = to === 0 ? { from } : { from, to };
382
+ return { className: "AlertQuery", props };
383
+ }
384
+
385
+ private rule(r: Json, gi: number, ri: number, unit: string): DeclRef | undefined {
386
+ const path: Array<string | number> = ["groups", gi, "rules", ri];
387
+ if (typeof r.title !== "string" || !Array.isArray(r.data)) {
388
+ this.report.drop(pointer(...path), `group ${gi}`, `rules[${ri}]`, "(it has no title or data)");
389
+ return undefined;
390
+ }
391
+ const subject = `alert rule "${r.title}"`;
392
+ const id = `rule:${gi}:${ri}`;
393
+ const data: DeclRef[] = [];
394
+ r.data.forEach((q, qi) => {
395
+ if (!isObject(q)) {
396
+ this.report.drop(pointer(...path, "data", qi), subject, `data[${qi}]`, "(it is not a query)");
397
+ return;
398
+ }
399
+ const { className, props } = this.query(q, [...path, "data", qi], qi, subject);
400
+ const qid = `${id}:${qi}`;
401
+ const refId = typeof q.refId === "string" ? q.refId : refIdAt(qi);
402
+ this.declarations.push({ id: qid, kind: "new", className, props, name: { of: id, suffix: refId }, module: "rules", unit });
403
+ data.push(declRef(qid));
404
+ });
405
+
406
+ const props: Json = { title: r.title };
407
+ if (typeof r.uid === "string") props.uid = r.uid;
408
+ const lastRef = (() => {
409
+ const last = r.data[r.data.length - 1];
410
+ return isObject(last) && typeof last.refId === "string" ? last.refId : refIdAt(r.data.length - 1);
411
+ })();
412
+ const isRecord = isObject(r.record);
413
+ if (typeof r.condition === "string" && r.condition !== "" && (isRecord || r.condition !== lastRef)) props.condition = r.condition;
414
+ props.data = data;
415
+ for (const k of RULE_FIELDS) {
416
+ if (["uid", "title", "condition", "data"].includes(k)) continue;
417
+ const v = r[k];
418
+ if (v === undefined || v === null) continue;
419
+ if (k === "dasboardUid") {
420
+ if (r.dashboardUid === undefined || r.dashboardUid === "") {
421
+ // Grafana reads the old misspelling as dashboardUid.
422
+ props.dashboardUid = v;
423
+ this.report.edit({ op: "replace", path: pointer(...path, "dashboardUid"), value: v });
424
+ this.report.drop(pointer(...path, k), subject, k);
425
+ } else this.report.drop(pointer(...path, k), subject, k, "(dashboardUid is set too, and Grafana uses that)");
426
+ continue;
427
+ }
428
+ if (k === "notification_settings" && isObject(v)) {
429
+ const ns: Json = {};
430
+ for (const [nk, nv] of Object.entries(v)) {
431
+ if (!NOTIFICATION_SETTINGS_FIELDS.includes(nk)) {
432
+ this.report.drop(pointer(...path, k, nk), subject, `notification_settings.${nk}`, NO_PROP);
433
+ continue;
434
+ }
435
+ ns[nk] = nk === "receiver" ? this.receiverRef(nv) : nk.endsWith("time_intervals") ? this.timingRefs(nv) : nv;
436
+ }
437
+ props.notification_settings = ns;
438
+ continue;
439
+ }
440
+ props[k] = v;
441
+ }
442
+ for (const k of Object.keys(r)) if (!RULE_FIELDS.includes(k)) this.report.drop(pointer(...path, k), subject, k, NO_PROP);
443
+ this.declarations.push({ id, kind: "new", className: "AlertRule", props, name: r.title, module: "rules", unit });
444
+ return declRef(id);
445
+ }
446
+
447
+ private groups(list: unknown[]): void {
448
+ list.forEach((g, gi) => {
449
+ if (!isObject(g) || typeof g.name !== "string" || typeof g.folder !== "string") {
450
+ this.report.drop(pointer("groups", gi), "groups", `entry ${gi}`, "(it has no name or folder)");
451
+ return;
452
+ }
453
+ const unit = `group:${gi}`;
454
+ const rules = (Array.isArray(g.rules) ? g.rules : []).flatMap((r, ri) => {
455
+ const ref = isObject(r) ? this.rule(r, gi, ri, unit) : undefined;
456
+ return ref ? [ref] : [];
457
+ });
458
+ const props: Json = {};
459
+ for (const k of GROUP_FIELDS) if (k !== "rules" && g[k] !== undefined && g[k] !== null) props[k] = g[k];
460
+ props.rules = rules;
461
+ for (const k of Object.keys(g)) if (!GROUP_FIELDS.includes(k)) this.report.drop(pointer("groups", gi, k), `rule group "${g.name}"`, k, NO_PROP);
462
+ this.declarations.push({ id: unit, kind: "new", className: "AlertRuleGroup", props, name: `${g.name} rules`, module: "rules", unit });
463
+ });
464
+ }
465
+
466
+ convert(): Plan {
467
+ const d = this.doc;
468
+ const groups = Array.isArray(d.groups) ? d.groups : [];
469
+ this.scanDatasources(groups);
470
+ this.templates(Array.isArray(d.templates) ? d.templates : []);
471
+ this.muteTimes(Array.isArray(d.muteTimes) ? d.muteTimes : []);
472
+ this.contactPointList(Array.isArray(d.contactPoints) ? d.contactPoints : []);
473
+ this.policies(Array.isArray(d.policies) ? d.policies : []);
474
+ this.groups(groups);
475
+ for (const [key, what] of Object.entries(DELETIONS)) {
476
+ const v = d[key];
477
+ if (!Array.isArray(v) || v.length === 0) continue;
478
+ this.report.drop(pointer(key), "the provisioning file", "", `lists ${v.length} ${what} (${key}), which chant does not write; apply them to Grafana once by hand or keep that file as it is`);
479
+ }
480
+ const exported = this.declarations.filter((x) => ["AlertRuleGroup", "ContactPoint", "NotificationPolicy", "MuteTiming", "NotificationTemplate"].includes(x.className ?? ""));
481
+ return {
482
+ directory: "",
483
+ modules: [
484
+ { key: "datasources", file: "alerting-datasources", summary: "Datasources the alert rules query, already in Grafana" },
485
+ { key: "notifications", file: "notifications", summary: "Contact points, notification policies, mute timings and templates, from a Grafana alerting provisioning file" },
486
+ { key: "rules", file: "alert-rules", summary: "Grafana-managed alert rules, from a Grafana alerting provisioning file" },
487
+ ],
488
+ declarations: this.declarations,
489
+ customClasses: [],
490
+ exports: exported.map((x) => x.id),
491
+ };
492
+ }
493
+ }
494
+
495
+ /** Plan an alerting provisioning file. `edits` applied to the file give what the rebuilt file should equal (see `normalizeAlerting`). */
496
+ export function planAlertingProvisioning(doc: Json): { plan: Plan; edits: ImportEdit[]; warnings: string[] } {
497
+ const report = new Report();
498
+ const plan = new AlertingConverter(doc, report).convert();
499
+ return { plan, edits: report.edits, warnings: report.warnings() };
500
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * What the importer did to a dashboard on the way to TypeScript, as edits
3
+ * to its JSON.
4
+ *
5
+ * Every key the importer leaves out, and every value it writes in another
6
+ * form, is one edit, and every edit that changes what Grafana does comes
7
+ * with an import warning. The round-trip test applies the edits to the
8
+ * source dashboard and expects the rebuilt dashboard to equal the result
9
+ * (after `normalizeDashboard`), which is how it checks that nothing is
10
+ * dropped without a warning.
11
+ */
12
+
13
+ import { isObject } from "./normalize";
14
+
15
+ type Json = Record<string, unknown>;
16
+
17
+ export type ImportEdit =
18
+ /** The value at `path` (a JSON pointer) is not carried. */
19
+ | { readonly op: "remove"; readonly path: string }
20
+ /** The value at `path` is written as `value`. */
21
+ | { readonly op: "replace"; readonly path: string; readonly value: unknown }
22
+ /** Every string in the dashboard has `from` replaced by `to` (an `__inputs` constant filled in). */
23
+ | { readonly op: "substitute"; readonly from: string; readonly to: string }
24
+ /** `value` is added at the front of `templating.list` (an `__inputs` datasource as a variable). */
25
+ | { readonly op: "prependVariable"; readonly value: Json };
26
+
27
+ /** `/a/b/0` as its unescaped segments. */
28
+ export function pointerSegments(path: string): string[] {
29
+ if (path === "") return [];
30
+ return path
31
+ .slice(1)
32
+ .split("/")
33
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
34
+ }
35
+
36
+ /** A JSON pointer from segments. */
37
+ export function pointer(...segments: Array<string | number>): string {
38
+ return segments.map((s) => `/${String(s).replace(/~/g, "~0").replace(/\//g, "~1")}`).join("");
39
+ }
40
+
41
+ function substitute(value: unknown, from: string, to: string): unknown {
42
+ if (typeof value === "string") return value.split(from).join(to);
43
+ if (Array.isArray(value)) return value.map((v) => substitute(v, from, to));
44
+ if (isObject(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, substitute(v, from, to)]));
45
+ return value;
46
+ }
47
+
48
+ const REMOVED = Symbol("removed");
49
+
50
+ function parentOf(root: unknown, segments: string[]): unknown {
51
+ let node = root;
52
+ for (const s of segments.slice(0, -1)) {
53
+ if (Array.isArray(node)) node = node[Number(s)];
54
+ else if (isObject(node)) node = node[s];
55
+ else return undefined;
56
+ }
57
+ return node;
58
+ }
59
+
60
+ function sweep(value: unknown): unknown {
61
+ if (Array.isArray(value)) return value.filter((v) => v !== REMOVED).map(sweep);
62
+ if (isObject(value)) {
63
+ const out: Json = {};
64
+ for (const [k, v] of Object.entries(value)) if (v !== REMOVED) out[k] = sweep(v);
65
+ return out;
66
+ }
67
+ return value;
68
+ }
69
+
70
+ /**
71
+ * The dashboard with the edits applied: substitutions first, then
72
+ * replacements and removals (all by pointers into the original), then the
73
+ * prepended variables.
74
+ */
75
+ export function applyEdits(dashboard: Json, edits: readonly ImportEdit[]): Json {
76
+ let doc = structuredClone(dashboard) as unknown;
77
+ for (const e of edits) if (e.op === "substitute") doc = substitute(doc, e.from, e.to);
78
+ for (const e of edits) {
79
+ if (e.op !== "replace" && e.op !== "remove") continue;
80
+ const segments = pointerSegments(e.path);
81
+ const parent = parentOf(doc, segments);
82
+ const last = segments[segments.length - 1];
83
+ const value = e.op === "replace" ? e.value : REMOVED;
84
+ if (Array.isArray(parent)) parent[Number(last)] = value;
85
+ else if (isObject(parent)) parent[last] = value;
86
+ }
87
+ const out = sweep(doc) as Json;
88
+ const prepend = edits.filter((e): e is Extract<ImportEdit, { op: "prependVariable" }> => e.op === "prependVariable").map((e) => e.value);
89
+ if (prepend.length > 0) {
90
+ const templating = isObject(out.templating) ? out.templating : {};
91
+ const list = Array.isArray(templating.list) ? templating.list : [];
92
+ out.templating = { ...templating, list: [...prepend, ...list] };
93
+ }
94
+ return out;
95
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { embeddedDocument, type EmbeddedContent } from "@intentius/chant/import/embedded";
3
+ import { grafanaPlugin } from "../plugin";
4
+ import { dashboardImporter } from "./embedded";
5
+
6
+ const DASHBOARD = JSON.stringify({
7
+ title: "API",
8
+ uid: "api",
9
+ schemaVersion: 41,
10
+ panels: [{ type: "stat", title: "Requests", gridPos: { h: 4, w: 6, x: 0, y: 0 }, targets: [{ refId: "A", expr: "sum(up)" }] }],
11
+ });
12
+
13
+ const site = (text: string): EmbeddedContent => ({
14
+ host: "k8s",
15
+ hostType: "K8s::Core::ConfigMap",
16
+ location: 'ConfigMap dashboards data["api.json"]',
17
+ directory: "dashboards",
18
+ text,
19
+ document: embeddedDocument(text),
20
+ labels: { grafana_dashboard: "1" },
21
+ });
22
+
23
+ describe("dashboard JSON embedded in another lexicon's resource (#2962)", () => {
24
+ test("the plugin registers the importer", () => {
25
+ expect(grafanaPlugin.embeddedImporters?.()).toEqual([dashboardImporter]);
26
+ });
27
+
28
+ test("matches classic dashboard JSON; not a v2 dashboard or other text", () => {
29
+ expect(dashboardImporter.matches(site(DASHBOARD))).toBe(true);
30
+ const v2 = JSON.stringify({ apiVersion: "dashboard.grafana.app/v2beta1", kind: "Dashboard", spec: { elements: {}, layout: {} } });
31
+ expect(dashboardImporter.matches(site(v2))).toBe(false);
32
+ expect(dashboardImporter.matches(site("apiVersion: 1\ndatasources: []\n"))).toBe(false);
33
+ });
34
+
35
+ test("imports the dashboard into the content's own directory, referenced through dashboardJson", () => {
36
+ const out = dashboardImporter.import(site(DASHBOARD));
37
+ // No second directory inside the one core gives it.
38
+ expect(out.files.every((f) => !f.path.includes("/"))).toBe(true);
39
+ expect(out.value.shape).toBe("single");
40
+ expect(out.value.through).toEqual({ from: "@intentius/chant-lexicon-grafana", name: "dashboardJson" });
41
+ const [binding] = out.value.bindings;
42
+ const file = out.files.find((f) => f.path === binding.from);
43
+ expect(file?.content).toContain(`const ${binding.name} = new Dashboard(`);
44
+ });
45
+ });