@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,255 @@
1
+ /**
2
+ * The thin read (#2946) against an in-memory Grafana, and the shared
3
+ * observation conformance suite over the same fake.
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import { describeObservationConformance } from "@intentius/chant-test-utils";
8
+ import { normalizeObservation } from "@intentius/chant/observation";
9
+ import { describeResources, type GrafanaObserveOptions } from "./describe-resources";
10
+ import { fakeGrafana, liveDatasource, type FakeGrafanaState } from "./api/fake-grafana";
11
+ import { grafanaPlugin } from "./plugin";
12
+ import { Dashboard, DashboardProvider } from "./dashboard";
13
+ import { Datasource, ExternalDatasource } from "./datasource";
14
+ import { StatPanel, Row } from "./panels";
15
+ import { PromQuery } from "./query";
16
+ import { CustomVariable } from "./variables";
17
+
18
+ type Entities = GrafanaObserveOptions["entities"];
19
+
20
+ const CONFIG = { grafana: { profiles: { prod: { url: "http://grafana.test", token: { env: "GRAFANA_TEST_TOKEN" } } } } };
21
+ const ENV = { GRAFANA_TEST_TOKEN: "glsa_test" };
22
+
23
+ const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });
24
+ const loki = new ExternalDatasource({ type: "loki", uid: "shared-loki" });
25
+ const up = new PromQuery({ expr: "up", datasource: prometheus });
26
+ const upPanel = new StatPanel({ title: "Up", targets: [up] });
27
+ const row = new Row({ title: "Health", panels: [upPanel] });
28
+ const env = new CustomVariable({ name: "env", values: ["prod", "staging"] });
29
+ const overview = new Dashboard({ title: "Overview", variables: [env], panels: [row] });
30
+ const legacy = new Dashboard({ title: "Legacy", uid: "legacy-board" });
31
+ const lonely = new StatPanel({ title: "On no dashboard" });
32
+ const provider = new DashboardProvider({ name: "ops" });
33
+
34
+ function entities(pairs: Record<string, { entityType: string; props: unknown }>): Entities {
35
+ return new Map(Object.entries(pairs).map(([n, e]) => [n, { entityType: e.entityType, props: e.props as Record<string, unknown> }]));
36
+ }
37
+
38
+ const ALL = entities({ overview, legacy, prometheus, loki, upPanel, up, row, env, provider });
39
+
40
+ function state(overrides: Partial<FakeGrafanaState> = {}): FakeGrafanaState {
41
+ return {
42
+ api: "v1",
43
+ dashboards: {
44
+ overview: { spec: { title: "Overview", panels: [] }, annotations: { "grafana.app/managedBy": "classic-file-provisioning", "grafana.app/managerId": "ops" } },
45
+ "legacy-board": { spec: { title: "Legacy", panels: [] }, labels: { "app.kubernetes.io/managed-by": "chant", "chant.intentius.io/stack": "shop", "chant.intentius.io/env": "prod" } },
46
+ },
47
+ datasources: { prometheus: liveDatasource({ name: "Prometheus", type: "prometheus", uid: "prometheus" }) },
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ function run(s: FakeGrafanaState, opts: Partial<GrafanaObserveOptions> = {}, calls?: string[]) {
53
+ return describeResources({
54
+ environment: "prod",
55
+ entityNames: [...ALL.keys()],
56
+ entities: ALL,
57
+ config: CONFIG,
58
+ env: ENV,
59
+ http: fakeGrafana(s, calls),
60
+ ...opts,
61
+ });
62
+ }
63
+
64
+ describe("describeResources", () => {
65
+ it("reads each dashboard and datasource by the uid the build gives it, and says where it looked", async () => {
66
+ const { resources, queried } = normalizeObservation(await run(state()));
67
+ expect(resources.overview).toMatchObject({ type: "Grafana::Dashboard", physicalId: "overview", status: "PRESENT", ownership: "owned" });
68
+ expect(resources.prometheus).toMatchObject({ physicalId: "prometheus", ownership: "unknown" });
69
+ expect(queried.overview).toBe("/apis/dashboard.grafana.app/v1/namespaces/default/dashboards/overview");
70
+ expect(queried.prometheus).toBe("/api/datasources/uid/prometheus");
71
+ });
72
+
73
+ it("uses v1beta1 on a 12.x server and /api/dashboards/uid on one without the dashboard API", async () => {
74
+ expect(normalizeObservation(await run(state({ api: "v1beta1" }))).queried.overview).toContain("/v1beta1/");
75
+ const legacyRead = normalizeObservation(await run(state({ api: "legacy" })));
76
+ expect(legacyRead.queried.overview).toBe("/api/dashboards/uid/overview");
77
+ expect(legacyRead.resources.overview.ownership).toBe("unknown");
78
+ });
79
+
80
+ it("a 404 is absent; an external datasource that is not there is absent too", async () => {
81
+ const { resources, unobserved } = normalizeObservation(await run(state()));
82
+ expect(resources.loki).toBeUndefined();
83
+ expect(unobserved.loki).toBeUndefined();
84
+ });
85
+
86
+ it("a panel, row, query or variable takes its dashboard's verdict, with one read of the dashboard", async () => {
87
+ const calls: string[] = [];
88
+ const { resources } = normalizeObservation(await run(state(), {}, calls));
89
+ for (const name of ["upPanel", "up", "row", "env"]) {
90
+ expect(resources[name], name).toMatchObject({ physicalId: "overview", ownership: "owned", attributes: { dashboard: "overview" } });
91
+ }
92
+ expect(calls.filter((c) => c.endsWith("/dashboards/overview"))).toHaveLength(1);
93
+ });
94
+
95
+ it("a part of a dashboard that is absent is absent", async () => {
96
+ const s = state();
97
+ delete s.dashboards.overview;
98
+ const { resources, unobserved } = normalizeObservation(await run(s));
99
+ for (const name of ["overview", "upPanel", "up", "row", "env"]) {
100
+ expect(resources[name], name).toBeUndefined();
101
+ expect(unobserved[name], name).toBeUndefined();
102
+ }
103
+ });
104
+
105
+ it("reads chant's marker, stack and env included, from the labels of a dashboard written through the API", async () => {
106
+ const { resources } = normalizeObservation(await run(state()));
107
+ expect(resources.legacy).toMatchObject({ ownership: "owned", marker: { stack: "shop", env: "prod" } });
108
+ });
109
+
110
+ it("a dashboard another provider loads is foreign", async () => {
111
+ const s = state();
112
+ s.dashboards.overview.annotations = { "grafana.app/managedBy": "classic-file-provisioning", "grafana.app/managerId": "other" };
113
+ expect(normalizeObservation(await run(s)).resources.overview.ownership).toBe("foreign");
114
+ });
115
+ });
116
+
117
+ describeObservationConformance({
118
+ lexicon: "grafana",
119
+ ownershipChannel: grafanaPlugin.ownershipChannel,
120
+ scenarios: [
121
+ {
122
+ name: "a provisioned estate on Grafana 13",
123
+ declared: [...ALL.keys()],
124
+ run: () => run(state()),
125
+ expectPresent: ["overview", "legacy", "prometheus", "upPanel", "up", "row", "env"],
126
+ expectAbsent: ["loki"],
127
+ expectUnobserved: ["provider"],
128
+ expectMarker: { legacy: { stack: "shop", env: "prod" } },
129
+ expectNoMarker: ["overview"],
130
+ },
131
+ {
132
+ name: "owned: true keeps chant's dashboards and withholds what it cannot show is chant's",
133
+ declared: [...ALL.keys()],
134
+ owned: true,
135
+ run: () => run(state(), { owned: true }),
136
+ expectPresent: ["overview", "legacy"],
137
+ expectUnobserved: ["prometheus", "provider"],
138
+ },
139
+ {
140
+ name: "owned: true on Grafana 11, where no dashboard's ownership can be read",
141
+ declared: ["overview", "legacy"],
142
+ owned: true,
143
+ run: () => run(state({ api: "legacy" }), { owned: true, entityNames: ["overview", "legacy"] }),
144
+ expectUnobserved: ["overview", "legacy"],
145
+ },
146
+ {
147
+ name: "a refused token",
148
+ declared: [...ALL.keys()],
149
+ run: () => run(state({ status: 401 })),
150
+ expectUnobserved: [...ALL.keys()],
151
+ },
152
+ {
153
+ name: "no Grafana bound to the environment",
154
+ declared: ["overview"],
155
+ run: () => describeResources({ environment: "staging", entityNames: ["overview"], entities: ALL, config: CONFIG, env: {} }),
156
+ expectUnobserved: ["overview"],
157
+ },
158
+ {
159
+ name: "a profile naming a token variable that is not set",
160
+ declared: ["overview"],
161
+ run: () => describeResources({ environment: "prod", entityNames: ["overview"], entities: ALL, config: CONFIG, env: {} }),
162
+ expectUnobserved: ["overview"],
163
+ },
164
+ {
165
+ name: "a dashboard stored as v2 that the server does not serve at v2",
166
+ declared: ["overview"],
167
+ run: () => {
168
+ const s = state();
169
+ s.dashboards.overview.storedVersion = "v2beta1";
170
+ return run(s, { entityNames: ["overview"] });
171
+ },
172
+ expectUnobserved: ["overview"],
173
+ },
174
+ {
175
+ name: "a panel on no dashboard",
176
+ declared: ["lonely"],
177
+ run: () => run(state(), { entityNames: ["lonely"], entities: new Map([...ALL, ["lonely", { entityType: lonely.entityType, props: lonely.props as unknown as Record<string, unknown> }]]) }),
178
+ expectUnobserved: ["lonely"],
179
+ },
180
+ ],
181
+ });
182
+
183
+ /** A small v2 spec of the "Overview" dashboard: one stat panel in a tab. */
184
+ const OVERVIEW_V2 = {
185
+ title: "Overview",
186
+ annotations: [],
187
+ cursorSync: "Off",
188
+ links: [],
189
+ preload: false,
190
+ tags: [],
191
+ variables: [],
192
+ timeSettings: { from: "now-6h", to: "now", autoRefresh: "", autoRefreshIntervals: [], hideTimepicker: false, fiscalYearStartMonth: 0 },
193
+ elements: {
194
+ "panel-1": {
195
+ kind: "Panel",
196
+ spec: { id: 1, title: "Up", description: "", links: [], data: { kind: "QueryGroup", spec: { queries: [], transformations: [], queryOptions: {} } }, vizConfig: { kind: "VizConfig", group: "stat", version: "", spec: { options: {}, fieldConfig: { defaults: {}, overrides: [] } } } },
197
+ },
198
+ },
199
+ layout: {
200
+ kind: "TabsLayout",
201
+ spec: { tabs: [{ kind: "TabsLayoutTab", spec: { title: "Health", layout: { kind: "GridLayout", spec: { items: [{ kind: "GridLayoutItem", spec: { x: 0, y: 0, width: 12, height: 8, element: { kind: "ElementReference", name: "panel-1" } } }] } } } }] },
202
+ },
203
+ };
204
+
205
+ describe("a dashboard Grafana stores as v2 (#2947)", () => {
206
+ for (const [api, version] of [["v1", "v2"], ["v1beta1", "v2beta1"]] as const) {
207
+ it(`is read again at ${version} on a ${api} server, and says it was v2 and what the classic form cannot hold`, async () => {
208
+ const s = state({ api });
209
+ s.dashboards.overview = { ...s.dashboards.overview, storedVersion: version, v2: OVERVIEW_V2 };
210
+ const calls: string[] = [];
211
+ const obs = normalizeObservation(await run(s, { entityNames: ["overview"] }, calls));
212
+ expect(obs.unobserved).toEqual({});
213
+ // Ownership comes from the v2 resource's metadata, as from the classic one: the declared ops provider loads it.
214
+ expect(obs.resources.overview).toMatchObject({ status: "PRESENT", ownership: "owned" });
215
+ expect(obs.resources.overview.attributes).toMatchObject({ title: "Overview", schema: "v2", v2Lossy: [expect.stringMatching(/tabs "Health" become expanded rows/)] });
216
+ expect(calls).toContain(`GET /apis/dashboard.grafana.app/${version}/namespaces/default/dashboards/overview`);
217
+ });
218
+ }
219
+
220
+ it("a classic dashboard is read once, at the classic version, with no schema attribute", async () => {
221
+ const calls: string[] = [];
222
+ const obs = normalizeObservation(await run(state(), { entityNames: ["overview"] }, calls));
223
+ expect(obs.resources.overview.attributes).not.toHaveProperty("schema");
224
+ expect(calls.filter((c) => c.includes("/dashboards/overview"))).toEqual(["GET /apis/dashboard.grafana.app/v1/namespaces/default/dashboards/overview"]);
225
+ });
226
+
227
+ it("one the server does not serve at v2 is unsupported-kind, not the lossy classic read", async () => {
228
+ const s = state();
229
+ s.dashboards.overview.storedVersion = "v2";
230
+ const obs = normalizeObservation(await run(s, { entityNames: ["overview"] }));
231
+ expect(obs.unobserved.overview).toMatchObject({ reason: "unsupported-kind", detail: expect.stringMatching(/stored as a v2 dashboard .* lossy down-conversion/) });
232
+ });
233
+
234
+ it("a failed v2 read is read-failed", async () => {
235
+ const s = state();
236
+ s.dashboards.overview = { ...s.dashboards.overview, storedVersion: "v2", v2: OVERVIEW_V2 };
237
+ const base = fakeGrafana(s);
238
+ const http: typeof base = async (method, path, body) => (path.includes("/v2/") ? { status: 500, json: { message: "boom" } } : base(method, path, body));
239
+ const obs = normalizeObservation(await describeResources({ environment: "prod", entityNames: ["overview"], entities: ALL, config: CONFIG, env: ENV, http }));
240
+ expect(obs.unobserved.overview.reason).toBe("read-failed");
241
+ });
242
+ });
243
+
244
+ describe("unobserved reasons", () => {
245
+ it("names the reason each hole has", async () => {
246
+ const refused = normalizeObservation(await run(state({ status: 401 })));
247
+ expect(refused.unobserved.overview.reason).toBe("no-credentials");
248
+ const unbound = normalizeObservation(await describeResources({ environment: "staging", entityNames: ["overview"], entities: ALL, config: CONFIG, env: {} }));
249
+ expect(unbound.unobserved.overview).toMatchObject({ reason: "no-binding", detail: expect.stringContaining("GRAFANA_URL") });
250
+ const noToken = normalizeObservation(await describeResources({ environment: "prod", entityNames: ["overview"], entities: ALL, config: CONFIG, env: {} }));
251
+ expect(noToken.unobserved.overview).toMatchObject({ reason: "no-credentials", detail: expect.stringContaining("GRAFANA_TEST_TOKEN") });
252
+ const failing = normalizeObservation(await run(state({ status: 500 })));
253
+ expect(failing.unobserved.overview.reason).toBe("read-failed");
254
+ });
255
+ });
@@ -0,0 +1,209 @@
1
+ /**
2
+ * `describeResources()` for grafana (#2946): does each declared dashboard
3
+ * and datasource exist in the environment's Grafana, and whose is it.
4
+ *
5
+ * Runs on core's observer harness. `bind()` resolves the environment's
6
+ * Grafana (`grafana.profiles.<env>`, else `GRAFANA_URL`) and discovers which
7
+ * dashboard API it serves; each entity is one GET, by the uid the build gives
8
+ * it. Verdicts:
9
+ *
10
+ * | Entity | Read | 404 | Not readable |
11
+ * |---|---|---|---|
12
+ * | `Dashboard` | `/apis/dashboard.grafana.app/<v>/.../dashboards/<uid>` (Grafana 11: `/api/dashboards/uid/<uid>`) | absent | a v2-stored dashboard the server does not serve at v2: `unsupported-kind` |
13
+ * | `Datasource`, `ExternalDatasource` | `/api/datasources/uid/<uid>` | absent | |
14
+ * | a panel, row, query or variable | its dashboard's read (./members.ts) | its dashboard's | its dashboard's |
15
+ * | `DashboardProvider` | none: a provisioning-file setting Grafana serves no API for | | `unsupported-kind` |
16
+ *
17
+ * A dashboard Grafana stores as v2 is read at v2 and converted (#2947);
18
+ * its metadata says `schema: "v2"` and lists what the classic form cannot
19
+ * hold under `v2Lossy`.
20
+ *
21
+ * A refused token is `no-credentials` for every entity, a missing binding
22
+ * `no-binding`, anything else `read-failed`, never absent.
23
+ *
24
+ * Ownership is read where Grafana has a channel (./ownership.ts): a
25
+ * dashboard over `/apis` resolves `owned` or `foreign`; a dashboard over the
26
+ * legacy API and every datasource say `unknown`, and an `owned: true` read
27
+ * withholds them as `filtered`, since it cannot show they are chant's.
28
+ */
29
+
30
+ import type { ResourceMetadata } from "@intentius/chant/lexicon";
31
+ import {
32
+ observeEntities,
33
+ type DeclaredEntity,
34
+ type DescribeResourcesResult,
35
+ type EntityObservation,
36
+ type ObserverAdapter,
37
+ } from "@intentius/chant/observation";
38
+ import { bindGrafana, classifyGrafanaFailure, type BindOptions } from "./api/bind";
39
+ import { GrafanaApiError, type GrafanaClient } from "./api/client";
40
+ import { classicDashboardOf, dashboardApi, readDashboard } from "./api/dashboards";
41
+ import { readDatasource } from "./api/datasources";
42
+ import { DASHBOARD_PROVIDER_TYPE, DASHBOARD_TYPE } from "./dashboard";
43
+ import { DATASOURCE_TYPE, EXTERNAL_DATASOURCE_TYPE } from "./datasource";
44
+ import { dashboardMembers, isDashboardPart } from "./members";
45
+ import { chantProviderNames, dashboardOwnership, ownershipGap } from "./ownership";
46
+ import { slugUid } from "./util";
47
+
48
+ export interface GrafanaObserveOptions extends Omit<BindOptions, "environment"> {
49
+ environment: string;
50
+ buildOutput?: string;
51
+ entityNames: string[];
52
+ entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
53
+ owned?: boolean;
54
+ }
55
+
56
+ /** The uid a declared dashboard or datasource has in Grafana: the one the build gives it. */
57
+ export function declaredUid(name: string, entityType: string, props: Record<string, unknown>): string | undefined {
58
+ if (typeof props.uid === "string" && props.uid !== "") return props.uid;
59
+ if (entityType === DASHBOARD_TYPE) return slugUid(name || String(props.title ?? ""));
60
+ if (entityType === DATASOURCE_TYPE && typeof props.name === "string") return slugUid(props.name);
61
+ return undefined;
62
+ }
63
+
64
+ /** What `unsupported-kind` says about a provider. */
65
+ export const PROVIDER_NOT_OBSERVABLE =
66
+ "a dashboard provider is a setting in Grafana's provisioning file, which Grafana serves no API for; its dashboards are observed instead";
67
+
68
+ /** What `unsupported-kind` says about a panel, row, query or variable that no declared dashboard holds. */
69
+ export const ORPHAN_PART =
70
+ "panels, rows, queries and variables exist in Grafana only inside a dashboard, and no declared dashboard holds this one";
71
+
72
+ /** A GrafanaApiError as the harness's per-entity verdict; anything else is rethrown for the harness to record. */
73
+ function unobservedFrom(err: unknown): EntityObservation {
74
+ if (err instanceof GrafanaApiError) return { unobserved: classifyGrafanaFailure(err) };
75
+ throw err;
76
+ }
77
+
78
+ async function observeDashboard(client: GrafanaClient, uid: string, providers: ReadonlySet<string>, owned: boolean | undefined): Promise<EntityObservation> {
79
+ let read;
80
+ try {
81
+ read = await readDashboard(client, uid);
82
+ } catch (err) {
83
+ return unobservedFrom(err);
84
+ }
85
+ if ("absent" in read) return { absent: true, queried: read.address };
86
+ const live = read.present;
87
+ const classic = classicDashboardOf(live);
88
+ if ("unsupported" in classic) {
89
+ return { unobserved: { reason: "unsupported-kind", detail: `dashboard "${uid}" is not readable: ${classic.unsupported}` }, queried: live.address };
90
+ }
91
+ const { ownership, marker } = dashboardOwnership(live, providers);
92
+ if (owned && ownership !== "owned") {
93
+ return {
94
+ unobserved: {
95
+ reason: "filtered",
96
+ detail: `dashboard "${uid}" exists but ${ownership === "unknown" ? "its ownership cannot be read" : "is not chant's"}: ${ownershipGap(live)}`,
97
+ },
98
+ queried: live.address,
99
+ };
100
+ }
101
+ const metadata = (live.raw.metadata ?? {}) as { generation?: unknown };
102
+ const legacyVersion = (live.raw.meta as { version?: unknown } | undefined)?.version;
103
+ const managerId = live.annotations["grafana.app/managerId"];
104
+ const meta: ResourceMetadata = {
105
+ type: DASHBOARD_TYPE,
106
+ physicalId: uid,
107
+ status: "PRESENT",
108
+ ownership,
109
+ ...(marker ? { marker } : {}),
110
+ attributes: {
111
+ title: classic.json.title,
112
+ via: live.via,
113
+ ...(live.folderUid ? { folderUid: live.folderUid } : {}),
114
+ // Grafana's save counter: a UI save moves it, so a snapshot diff sees the edit too.
115
+ ...(typeof metadata.generation === "number" ? { generation: metadata.generation } : {}),
116
+ ...(typeof legacyVersion === "number" ? { version: legacyVersion } : {}),
117
+ ...(managerId ? { managerId } : {}),
118
+ ...(classic.warnings !== undefined ? { schema: "v2", ...(classic.warnings.length > 0 ? { v2Lossy: classic.warnings } : {}) } : {}),
119
+ },
120
+ };
121
+ return { present: meta, queried: live.address };
122
+ }
123
+
124
+ async function observeDatasource(client: GrafanaClient, entity: DeclaredEntity, uid: string, owned: boolean | undefined): Promise<EntityObservation> {
125
+ let read;
126
+ try {
127
+ read = await readDatasource(client, uid);
128
+ } catch (err) {
129
+ return unobservedFrom(err);
130
+ }
131
+ if ("absent" in read) return { absent: true, queried: read.address };
132
+ if (owned) {
133
+ return {
134
+ unobserved: { reason: "filtered", detail: `datasource "${uid}" exists but its ownership cannot be read: Grafana's datasource API returns no labels or annotations` },
135
+ queried: read.address,
136
+ };
137
+ }
138
+ const ds = read.present;
139
+ return {
140
+ present: {
141
+ type: entity.type,
142
+ physicalId: uid,
143
+ status: "PRESENT",
144
+ ownership: "unknown",
145
+ attributes: {
146
+ name: ds.name,
147
+ type: ds.type,
148
+ ...(typeof ds.readOnly === "boolean" ? { readOnly: ds.readOnly } : {}),
149
+ ...(typeof ds.version === "number" ? { version: ds.version } : {}),
150
+ },
151
+ },
152
+ queried: read.address,
153
+ };
154
+ }
155
+
156
+ function adapter(options: GrafanaObserveOptions): ObserverAdapter<GrafanaClient> {
157
+ const providers = chantProviderNames(options.entities.values());
158
+ const members = dashboardMembers(options.entities);
159
+ // One read per dashboard, however many of its members are asked about.
160
+ const dashboardOnce = (client: GrafanaClient, uid: string) => client.once(`observe:${uid}`, () => observeDashboard(client, uid, providers, options.owned));
161
+
162
+ return {
163
+ async bind() {
164
+ const client = await bindGrafana({ ...options });
165
+ // Discovery is the first request, so a refused token or an unreachable
166
+ // server fails the bind: every entity NOT-OBSERVED with one reason.
167
+ await dashboardApi(client);
168
+ return client;
169
+ },
170
+ classifyBindFailure: (err) => classifyGrafanaFailure(err),
171
+ async read(client, entity): Promise<EntityObservation> {
172
+ if (entity.type === DASHBOARD_PROVIDER_TYPE) {
173
+ return { unobserved: { reason: "unsupported-kind", detail: PROVIDER_NOT_OBSERVABLE } };
174
+ }
175
+ if (isDashboardPart(entity.type)) {
176
+ const holder = members.get(entity.name);
177
+ const dashboard = holder !== undefined ? options.entities.get(holder) : undefined;
178
+ const uid = holder !== undefined && dashboard ? declaredUid(holder, DASHBOARD_TYPE, dashboard.props) : undefined;
179
+ if (!uid) return { unobserved: { reason: "unsupported-kind", detail: ORPHAN_PART } };
180
+ const verdict = await dashboardOnce(client, uid);
181
+ if (!("present" in verdict)) return verdict;
182
+ const { ownership, marker } = verdict.present;
183
+ return {
184
+ present: { type: entity.type, physicalId: uid, status: "PRESENT", ...(ownership ? { ownership } : {}), ...(marker ? { marker } : {}), attributes: { dashboard: uid } },
185
+ ...(verdict.queried ? { queried: verdict.queried } : {}),
186
+ };
187
+ }
188
+ const uid = declaredUid(entity.name, entity.type, entity.props);
189
+ if (entity.type === DASHBOARD_TYPE) {
190
+ return uid ? dashboardOnce(client, uid) : { unobserved: { reason: "read-failed", detail: `"${entity.name}" does not resolve to a uid` } };
191
+ }
192
+ if (entity.type === DATASOURCE_TYPE || entity.type === EXTERNAL_DATASOURCE_TYPE) {
193
+ return uid ? observeDatasource(client, entity, uid, options.owned) : { unobserved: { reason: "read-failed", detail: `"${entity.name}" does not resolve to a uid` } };
194
+ }
195
+ return { unobserved: { reason: "unsupported-kind", detail: `no grafana reader for ${entity.type}` } };
196
+ },
197
+ };
198
+ }
199
+
200
+ /** Observe every declared grafana entity against the environment's Grafana. */
201
+ export async function describeResources(options: GrafanaObserveOptions): Promise<DescribeResourcesResult> {
202
+ const declared: DeclaredEntity[] = [];
203
+ for (const name of options.entityNames) {
204
+ const entity = options.entities.get(name);
205
+ if (!entity) continue;
206
+ declared.push({ name, type: entity.entityType, props: entity.props });
207
+ }
208
+ return observeEntities(declared, adapter(options));
209
+ }
package/src/detect.ts CHANGED
@@ -14,6 +14,33 @@ export function looksLikeDashboard(data: unknown): data is Record<string, unknow
14
14
  return Array.isArray(data.panels) && (typeof data.schemaVersion === "number" || isObject(data.templating));
15
15
  }
16
16
 
17
+ /** A dashboard saved before Grafana 5.0: panels inside a top-level `rows` list. */
18
+ export function looksLikeLegacyRowsDashboard(data: unknown): data is Record<string, unknown> {
19
+ return isObject(data) && !Array.isArray(data.panels) && Array.isArray(data.rows) && typeof data.schemaVersion === "number";
20
+ }
21
+
22
+ /** A dashboard as the `dashboard.grafana.app` API serves it: `apiVersion`, `kind: Dashboard` and a `spec`. */
23
+ export function looksLikeDashboardResource(data: unknown): data is { apiVersion: string; kind: "Dashboard"; spec: Record<string, unknown> } {
24
+ return (
25
+ isObject(data) &&
26
+ data.kind === "Dashboard" &&
27
+ typeof data.apiVersion === "string" &&
28
+ data.apiVersion.startsWith("dashboard.grafana.app/") &&
29
+ isObject(data.spec)
30
+ );
31
+ }
32
+
33
+ /** A v2 dashboard: a `dashboard.grafana.app/v2*` resource, or a bare v2 spec (`elements` and `layout`). */
34
+ export function looksLikeV2Dashboard(data: unknown): boolean {
35
+ if (looksLikeDashboardResource(data)) return /^dashboard\.grafana\.app\/v2/.test(data.apiVersion);
36
+ return isObject(data) && isObject(data.elements) && isObject(data.layout);
37
+ }
38
+
39
+ /** What `GET /api/dashboards/uid/<uid>` returns: `{ dashboard, meta }`. */
40
+ export function looksLikeDashboardApiResponse(data: unknown): data is { dashboard: Record<string, unknown>; meta: unknown } {
41
+ return isObject(data) && isObject(data.meta) && (looksLikeDashboard(data.dashboard) || looksLikeLegacyRowsDashboard(data.dashboard));
42
+ }
43
+
17
44
  /** A datasource provisioning file: `apiVersion` and a `datasources` list. */
18
45
  export function looksLikeDatasourceProvisioning(data: unknown): data is { apiVersion: unknown; datasources: unknown[] } {
19
46
  return isObject(data) && data.apiVersion !== undefined && Array.isArray(data.datasources);
@@ -24,7 +51,57 @@ export function looksLikeDashboardProvisioning(data: unknown): data is { apiVers
24
51
  return isObject(data) && data.apiVersion !== undefined && Array.isArray(data.providers);
25
52
  }
26
53
 
27
- /** Template detection for `chant import` and friends: any of the three. */
54
+ /** The lists an alerting provisioning file holds (Grafana's `AlertingFileV1`). */
55
+ const ALERTING_LISTS = [
56
+ "groups",
57
+ "deleteRules",
58
+ "contactPoints",
59
+ "deleteContactPoints",
60
+ "policies",
61
+ "resetPolicies",
62
+ "muteTimes",
63
+ "deleteMuteTimes",
64
+ "templates",
65
+ "deleteTemplates",
66
+ ] as const;
67
+
68
+ /** A Grafana-managed rule group: a `folder`, or rules with `data` (queries), which a Prometheus rule group never has. */
69
+ function looksLikeGrafanaRuleGroup(g: unknown): boolean {
70
+ if (!isObject(g)) return false;
71
+ if (typeof g.folder === "string") return true;
72
+ return Array.isArray(g.rules) && g.rules.some((r) => isObject(r) && Array.isArray(r.data));
73
+ }
74
+
75
+ /**
76
+ * An alerting provisioning file (`provisioning/alerting/*.yaml`, or what
77
+ * `/api/v1/provisioning/*\/export` writes): at least one of the alerting
78
+ * lists at the top level and nothing but those and `apiVersion`. A `groups`
79
+ * list must hold Grafana rule groups, so a Prometheus rule file (`groups:`
80
+ * of `name` and `rules` with `expr`) is not mistaken for one.
81
+ */
82
+ export function looksLikeAlertingProvisioning(data: unknown): data is Record<string, unknown> {
83
+ if (!isObject(data)) return false;
84
+ const keys = Object.keys(data);
85
+ if (!keys.some((k) => (ALERTING_LISTS as readonly string[]).includes(k) && Array.isArray(data[k]))) return false;
86
+ if (!keys.every((k) => k === "apiVersion" || (ALERTING_LISTS as readonly string[]).includes(k))) return false;
87
+ if (Array.isArray(data.groups) && data.groups.length > 0 && !data.groups.every(looksLikeGrafanaRuleGroup)) return false;
88
+ return data.apiVersion !== undefined || Array.isArray(data.groups);
89
+ }
90
+
91
+ /**
92
+ * Template detection for `chant import` and friends: dashboard JSON in any
93
+ * of the shapes above (v2 included, #2947), or a datasource, dashboard or
94
+ * alerting provisioning file.
95
+ */
28
96
  export function detectTemplate(data: unknown): boolean {
29
- return looksLikeDashboard(data) || looksLikeDatasourceProvisioning(data) || looksLikeDashboardProvisioning(data);
97
+ return (
98
+ looksLikeDashboard(data) ||
99
+ looksLikeLegacyRowsDashboard(data) ||
100
+ looksLikeDashboardResource(data) ||
101
+ looksLikeV2Dashboard(data) ||
102
+ looksLikeDashboardApiResponse(data) ||
103
+ looksLikeDatasourceProvisioning(data) ||
104
+ looksLikeDashboardProvisioning(data) ||
105
+ looksLikeAlertingProvisioning(data)
106
+ );
30
107
  }