@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.
- package/README.md +15 -5
- package/dist/actions/grafana.d.ts +24 -0
- package/dist/actions/grafana.d.ts.map +1 -0
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/alerting-build.d.ts +128 -0
- package/dist/alerting-build.d.ts.map +1 -0
- package/dist/alerting.d.ts +273 -0
- package/dist/alerting.d.ts.map +1 -0
- package/dist/api/apply.d.ts +156 -0
- package/dist/api/apply.d.ts.map +1 -0
- package/dist/api/bind.d.ts +32 -0
- package/dist/api/bind.d.ts.map +1 -0
- package/dist/api/client.d.ts +76 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/converge.d.ts +33 -0
- package/dist/api/converge.d.ts.map +1 -0
- package/dist/api/dashboards.d.ts +102 -0
- package/dist/api/dashboards.d.ts.map +1 -0
- package/dist/api/datasources.d.ts +26 -0
- package/dist/api/datasources.d.ts.map +1 -0
- package/dist/api/fake-grafana-writes.d.ts +41 -0
- package/dist/api/fake-grafana-writes.d.ts.map +1 -0
- package/dist/api/fake-grafana.d.ts +61 -0
- package/dist/api/fake-grafana.d.ts.map +1 -0
- package/dist/api/folders.d.ts +110 -0
- package/dist/api/folders.d.ts.map +1 -0
- package/dist/api/library-panels.d.ts +52 -0
- package/dist/api/library-panels.d.ts.map +1 -0
- package/dist/build.d.ts +30 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/schema-types.d.ts +3 -3
- package/dist/codegen/schema-types.d.ts.map +1 -1
- package/dist/composites/agent-dashboard.d.ts.map +1 -1
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/index.d.ts +4 -2
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/red-dashboard.d.ts +21 -2
- package/dist/composites/red-dashboard.d.ts.map +1 -1
- package/dist/composites/shared.d.ts +7 -0
- package/dist/composites/shared.d.ts.map +1 -1
- package/dist/composites/slo-alert-rules.d.ts +92 -0
- package/dist/composites/slo-alert-rules.d.ts.map +1 -0
- package/dist/config.d.ts +111 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/contact-point-secrets.d.ts +17 -0
- package/dist/contact-point-secrets.d.ts.map +1 -0
- package/dist/dashboard.d.ts +16 -2
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/datasource-refs.d.ts +95 -0
- package/dist/datasource-refs.d.ts.map +1 -0
- package/dist/datasource.d.ts +29 -0
- package/dist/datasource.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts +64 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +42 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +49 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/detect.d.ts +28 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/export-resources.d.ts +38 -0
- package/dist/export-resources.d.ts.map +1 -0
- package/dist/import/alerting.d.ts +40 -0
- package/dist/import/alerting.d.ts.map +1 -0
- package/dist/import/edits.d.ts +47 -0
- package/dist/import/edits.d.ts.map +1 -0
- package/dist/import/embedded.d.ts +14 -0
- package/dist/import/embedded.d.ts.map +1 -0
- package/dist/import/generator.d.ts +60 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/live-export.d.ts +108 -0
- package/dist/import/live-export.d.ts.map +1 -0
- package/dist/import/mappings.d.ts +83 -0
- package/dist/import/mappings.d.ts.map +1 -0
- package/dist/import/model.d.ts +98 -0
- package/dist/import/model.d.ts.map +1 -0
- package/dist/import/normalize-alerting.d.ts +26 -0
- package/dist/import/normalize-alerting.d.ts.map +1 -0
- package/dist/import/normalize.d.ts +74 -0
- package/dist/import/normalize.d.ts.map +1 -0
- package/dist/import/parser.d.ts +110 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/import/report.d.ts +26 -0
- package/dist/import/report.d.ts.map +1 -0
- package/dist/import/testdata/fixtures.d.ts +35 -0
- package/dist/import/testdata/fixtures.d.ts.map +1 -0
- package/dist/import/v2.d.ts +72 -0
- package/dist/import/v2.d.ts.map +1 -0
- package/dist/index.d.ts +26 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +18 -10
- package/dist/k8s.d.ts +107 -0
- package/dist/k8s.d.ts.map +1 -0
- package/dist/lint/audit-catalog.d.ts +2 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/graf101.d.ts +2 -2
- package/dist/lint/post-synth/graf102.d.ts +1 -1
- package/dist/lint/post-synth/graf107.d.ts +1 -1
- package/dist/lint/post-synth/graf108.d.ts +8 -0
- package/dist/lint/post-synth/graf108.d.ts.map +1 -0
- package/dist/lint/post-synth/graf109.d.ts +8 -0
- package/dist/lint/post-synth/graf109.d.ts.map +1 -0
- package/dist/lint/post-synth/graf111.d.ts +8 -0
- package/dist/lint/post-synth/graf111.d.ts.map +1 -0
- package/dist/lint/post-synth/graf112.d.ts +8 -0
- package/dist/lint/post-synth/graf112.d.ts.map +1 -0
- package/dist/lint/post-synth/graf113.d.ts +8 -0
- package/dist/lint/post-synth/graf113.d.ts.map +1 -0
- package/dist/lint/post-synth/graf114.d.ts +8 -0
- package/dist/lint/post-synth/graf114.d.ts.map +1 -0
- package/dist/lint/post-synth/graf115.d.ts +8 -0
- package/dist/lint/post-synth/graf115.d.ts.map +1 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts +7 -5
- package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/rules/literal-secret.d.ts +7 -3
- package/dist/lint/rules/literal-secret.d.ts.map +1 -1
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/members.d.ts +27 -0
- package/dist/members.d.ts.map +1 -0
- package/dist/meta.json +135 -0
- package/dist/okf/index.md +36 -2
- package/dist/okf/rules/GRAF002.md +3 -2
- package/dist/okf/rules/GRAF101.md +3 -2
- package/dist/okf/rules/GRAF102.md +1 -0
- package/dist/okf/rules/GRAF108.md +11 -0
- package/dist/okf/rules/GRAF109.md +15 -0
- package/dist/okf/rules/GRAF111.md +11 -0
- package/dist/okf/rules/GRAF112.md +16 -0
- package/dist/okf/rules/GRAF113.md +16 -0
- package/dist/okf/rules/GRAF114.md +11 -0
- package/dist/okf/rules/GRAF115.md +11 -0
- package/dist/okf/types/AlertListPanel.md +9 -0
- package/dist/okf/types/AlertQuery.md +9 -0
- package/dist/okf/types/AlertRule.md +9 -0
- package/dist/okf/types/AlertRuleGroup.md +9 -0
- package/dist/okf/types/BarChartPanel.md +9 -0
- package/dist/okf/types/BarGaugePanel.md +9 -0
- package/dist/okf/types/CanvasPanel.md +9 -0
- package/dist/okf/types/ClassicConditionsExpression.md +9 -0
- package/dist/okf/types/ContactPoint.md +14 -0
- package/dist/okf/types/Dashboard.md +1 -0
- package/dist/okf/types/Datasource.md +3 -2
- package/dist/okf/types/ExternalDatasource.md +15 -0
- package/dist/okf/types/FlameGraphPanel.md +9 -0
- package/dist/okf/types/GeomapPanel.md +9 -0
- package/dist/okf/types/HistogramPanel.md +9 -0
- package/dist/okf/types/MathExpression.md +9 -0
- package/dist/okf/types/MuteTiming.md +13 -0
- package/dist/okf/types/NodeGraphPanel.md +9 -0
- package/dist/okf/types/NotificationPolicy.md +9 -0
- package/dist/okf/types/NotificationTemplate.md +9 -0
- package/dist/okf/types/PieChartPanel.md +9 -0
- package/dist/okf/types/ReduceExpression.md +9 -0
- package/dist/okf/types/ResampleExpression.md +9 -0
- package/dist/okf/types/SqlExpression.md +9 -0
- package/dist/okf/types/StateTimelinePanel.md +9 -0
- package/dist/okf/types/StatusHistoryPanel.md +9 -0
- package/dist/okf/types/ThresholdExpression.md +9 -0
- package/dist/okf/types/TrendPanel.md +9 -0
- package/dist/okf/types/XYChartPanel.md +9 -0
- package/dist/op/activities/grafana-apply.d.ts +75 -0
- package/dist/op/activities/grafana-apply.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +9 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/builders.d.ts +24 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/ownership.d.ts +67 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/panel-options.d.ts +65 -0
- package/dist/panel-options.d.ts.map +1 -0
- package/dist/panels.d.ts +46 -9
- package/dist/panels.d.ts.map +1 -1
- package/dist/pin.d.ts +29 -7
- package/dist/pin.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/promql-check.d.ts +51 -0
- package/dist/promql-check.d.ts.map +1 -0
- package/dist/query.d.ts +9 -8
- package/dist/query.d.ts.map +1 -1
- package/dist/rules/graf101.ts +3 -3
- package/dist/rules/graf102.ts +1 -1
- package/dist/rules/graf107.ts +1 -1
- package/dist/rules/graf108.ts +17 -0
- package/dist/rules/graf109.ts +17 -0
- package/dist/rules/graf111.ts +17 -0
- package/dist/rules/graf112.ts +17 -0
- package/dist/rules/graf113.ts +17 -0
- package/dist/rules/graf114.ts +17 -0
- package/dist/rules/graf115.ts +17 -0
- package/dist/rules/grafana-helpers.ts +15 -8
- package/dist/rules/literal-secret.ts +60 -6
- package/dist/schema/barchart.gen.d.ts +198 -0
- package/dist/schema/barchart.gen.d.ts.map +1 -0
- package/dist/schema/bargauge.gen.d.ts +97 -0
- package/dist/schema/bargauge.gen.d.ts.map +1 -0
- package/dist/schema/canvas.gen.d.ts +152 -0
- package/dist/schema/canvas.gen.d.ts.map +1 -0
- package/dist/schema/dashboard.gen.d.ts +196 -21
- package/dist/schema/dashboard.gen.d.ts.map +1 -1
- package/dist/schema/expr.gen.d.ts +606 -0
- package/dist/schema/expr.gen.d.ts.map +1 -0
- package/dist/schema/gauge.gen.d.ts.map +1 -1
- package/dist/schema/geomap.gen.d.ts +100 -0
- package/dist/schema/geomap.gen.d.ts.map +1 -0
- package/dist/schema/heatmap.gen.d.ts.map +1 -1
- package/dist/schema/histogram.gen.d.ts +132 -0
- package/dist/schema/histogram.gen.d.ts.map +1 -0
- package/dist/schema/index.d.ts +12 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/logs.gen.d.ts +1 -0
- package/dist/schema/logs.gen.d.ts.map +1 -1
- package/dist/schema/loki.gen.d.ts.map +1 -1
- package/dist/schema/nodegraph.gen.d.ts +49 -0
- package/dist/schema/nodegraph.gen.d.ts.map +1 -0
- package/dist/schema/piechart.gen.d.ts +127 -0
- package/dist/schema/piechart.gen.d.ts.map +1 -0
- package/dist/schema/prometheus.gen.d.ts.map +1 -1
- package/dist/schema/stat.gen.d.ts.map +1 -1
- package/dist/schema/statetimeline.gen.d.ts +124 -0
- package/dist/schema/statetimeline.gen.d.ts.map +1 -0
- package/dist/schema/statushistory.gen.d.ts +114 -0
- package/dist/schema/statushistory.gen.d.ts.map +1 -0
- package/dist/schema/table.gen.d.ts +22 -3
- package/dist/schema/table.gen.d.ts.map +1 -1
- package/dist/schema/tempo.gen.d.ts.map +1 -1
- package/dist/schema/text.gen.d.ts.map +1 -1
- package/dist/schema/timeseries.gen.d.ts +5 -1
- package/dist/schema/timeseries.gen.d.ts.map +1 -1
- package/dist/schema/trend.gen.d.ts +180 -0
- package/dist/schema/trend.gen.d.ts.map +1 -0
- package/dist/schema/xychart.gen.d.ts +145 -0
- package/dist/schema/xychart.gen.d.ts.map +1 -0
- package/dist/schema-validate.d.ts +30 -4
- package/dist/schema-validate.d.ts.map +1 -1
- package/dist/serializer.d.ts +11 -3
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-grafana-alerting.md +49 -0
- package/dist/skills/chant-grafana-provisioning.md +3 -3
- package/dist/skills/chant-grafana.md +12 -6
- package/dist/spec/fetch-units-cli.d.ts +3 -0
- package/dist/spec/fetch-units-cli.d.ts.map +1 -0
- package/dist/spec/overlay.d.ts +43 -0
- package/dist/spec/overlay.d.ts.map +1 -0
- package/dist/spec/schemas.d.ts +13 -7
- package/dist/spec/schemas.d.ts.map +1 -1
- package/dist/spec/units.d.ts +35 -0
- package/dist/spec/units.d.ts.map +1 -0
- package/dist/spec/units.gen.d.ts +2 -0
- package/dist/spec/units.gen.d.ts.map +1 -0
- package/dist/transformations.d.ts +459 -0
- package/dist/transformations.d.ts.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/validate-alerting.d.ts +65 -0
- package/dist/validate-alerting.d.ts.map +1 -0
- package/dist/validate-output.d.ts +49 -9
- package/dist/validate-output.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/variables.d.ts +7 -2
- package/dist/variables.d.ts.map +1 -1
- package/package.json +12 -4
- package/src/actions/actions.test.ts +24 -0
- package/src/actions/grafana.ts +40 -0
- package/src/actions/index.ts +1 -0
- package/src/alerting-build.ts +376 -0
- package/src/alerting.e2e.test.ts +212 -0
- package/src/alerting.test.ts +272 -0
- package/src/alerting.ts +435 -0
- package/src/api/apply.test.ts +238 -0
- package/src/api/apply.ts +384 -0
- package/src/api/bind.ts +58 -0
- package/src/api/client.ts +148 -0
- package/src/api/converge.ts +63 -0
- package/src/api/dashboards.ts +247 -0
- package/src/api/datasources.ts +33 -0
- package/src/api/fake-grafana-writes.ts +201 -0
- package/src/api/fake-grafana.ts +186 -0
- package/src/api/folders.ts +265 -0
- package/src/api/library-panels.ts +101 -0
- package/src/build.test.ts +210 -5
- package/src/build.ts +180 -43
- package/src/catalog.ts +86 -2
- package/src/codegen/docs.ts +42 -9
- package/src/codegen/schema-types.ts +16 -6
- package/src/composites/agent-dashboard.ts +6 -3
- package/src/composites/catalog.test.ts +2 -2
- package/src/composites/catalog.ts +82 -0
- package/src/composites/composites.test.ts +73 -9
- package/src/composites/index.ts +4 -2
- package/src/composites/queries.e2e.test.ts +374 -0
- package/src/composites/red-dashboard.ts +83 -12
- package/src/composites/shared.ts +11 -0
- package/src/composites/slo-alert-rules.ts +149 -0
- package/src/config.test.ts +61 -0
- package/src/config.ts +138 -0
- package/src/contact-point-secrets.ts +63 -0
- package/src/dashboard.ts +16 -2
- package/src/datasource-refs.test.ts +56 -0
- package/src/datasource-refs.ts +189 -0
- package/src/datasource.ts +52 -0
- package/src/deep-observe-hooks.ts +252 -0
- package/src/deep-observe.test.ts +265 -0
- package/src/deep-observe.ts +164 -0
- package/src/describe-resources.test.ts +255 -0
- package/src/describe-resources.ts +209 -0
- package/src/detect.ts +79 -2
- package/src/export-resources.test.ts +129 -0
- package/src/export-resources.ts +84 -0
- package/src/generated/lexicon-grafana.json +135 -0
- package/src/import/alerting-roundtrip.test.ts +161 -0
- package/src/import/alerting.ts +500 -0
- package/src/import/edits.ts +95 -0
- package/src/import/embedded.test.ts +45 -0
- package/src/import/embedded.ts +47 -0
- package/src/import/generated-types.e2e.test.ts +118 -0
- package/src/import/generator.test.ts +210 -0
- package/src/import/generator.ts +462 -0
- package/src/import/live-export.ts +264 -0
- package/src/import/mappings.ts +264 -0
- package/src/import/model.ts +110 -0
- package/src/import/normalize-alerting.ts +101 -0
- package/src/import/normalize.test.ts +117 -0
- package/src/import/normalize.ts +364 -0
- package/src/import/parser.test.ts +440 -0
- package/src/import/parser.ts +1236 -0
- package/src/import/report.ts +62 -0
- package/src/import/roundtrip.test.ts +468 -0
- package/src/import/testdata/fixtures.ts +129 -0
- package/src/import/v2.test.ts +292 -0
- package/src/import/v2.ts +803 -0
- package/src/import.e2e.test.ts +31 -84
- package/src/import.test.ts +9 -3
- package/src/index.ts +190 -5
- package/src/k8s.test.ts +98 -0
- package/src/k8s.ts +197 -0
- package/src/lint/audit-catalog.ts +60 -3
- package/src/lint/core-lint.test.ts +43 -0
- package/src/lint/post-synth/graf101.ts +3 -3
- package/src/lint/post-synth/graf102.ts +1 -1
- package/src/lint/post-synth/graf107.ts +1 -1
- package/src/lint/post-synth/graf108.ts +17 -0
- package/src/lint/post-synth/graf109.ts +17 -0
- package/src/lint/post-synth/graf111.ts +17 -0
- package/src/lint/post-synth/graf112.ts +17 -0
- package/src/lint/post-synth/graf113.ts +17 -0
- package/src/lint/post-synth/graf114.ts +17 -0
- package/src/lint/post-synth/graf115.ts +17 -0
- package/src/lint/post-synth/grafana-helpers.ts +15 -8
- package/src/lint/post-synth/index.ts +14 -0
- package/src/lint/post-synth/post-synth.test.ts +235 -11
- package/src/lint/rules/literal-secret.ts +60 -6
- package/src/lint/rules/rules.test.ts +27 -0
- package/src/lsp/completions.test.ts +5 -0
- package/src/lsp/hover.test.ts +7 -0
- package/src/lsp/hover.ts +8 -1
- package/src/members.ts +68 -0
- package/src/observe.e2e.test.ts +255 -0
- package/src/op/activities/grafana-apply.e2e.test.ts +175 -0
- package/src/op/activities/grafana-apply.test.ts +229 -0
- package/src/op/activities/grafana-apply.ts +171 -0
- package/src/op/activities/index.ts +8 -0
- package/src/op/builders.test.ts +40 -0
- package/src/op/builders.ts +34 -0
- package/src/ownership.ts +103 -0
- package/src/panel-options.ts +66 -0
- package/src/panels.test.ts +51 -0
- package/src/panels.ts +148 -10
- package/src/pin.ts +57 -6
- package/src/plugin.test.ts +5 -2
- package/src/plugin.ts +66 -0
- package/src/promql-check.test.ts +108 -0
- package/src/promql-check.ts +189 -0
- package/src/query.ts +13 -8
- package/src/schema/barchart.gen.ts +221 -0
- package/src/schema/bargauge.gen.ts +111 -0
- package/src/schema/canvas.gen.ts +176 -0
- package/src/schema/dashboard.gen.ts +206 -22
- package/src/schema/expr.gen.ts +618 -0
- package/src/schema/gauge.gen.ts +2 -1
- package/src/schema/geomap.gen.ts +112 -0
- package/src/schema/heatmap.gen.ts +2 -1
- package/src/schema/histogram.gen.ts +151 -0
- package/src/schema/index.ts +12 -0
- package/src/schema/logs.gen.ts +3 -1
- package/src/schema/loki.gen.ts +2 -1
- package/src/schema/nodegraph.gen.ts +58 -0
- package/src/schema/piechart.gen.ts +146 -0
- package/src/schema/prometheus.gen.ts +2 -1
- package/src/schema/stat.gen.ts +2 -1
- package/src/schema/statetimeline.gen.ts +143 -0
- package/src/schema/statushistory.gen.ts +132 -0
- package/src/schema/table.gen.ts +27 -4
- package/src/schema/tempo.gen.ts +2 -1
- package/src/schema/text.gen.ts +2 -1
- package/src/schema/timeseries.gen.ts +7 -2
- package/src/schema/trend.gen.ts +208 -0
- package/src/schema/xychart.gen.ts +167 -0
- package/src/schema-overlay.test.ts +282 -0
- package/src/schema-validate.ts +141 -50
- package/src/schema.test.ts +4 -2
- package/src/serializer.test.ts +13 -1
- package/src/serializer.ts +11 -3
- package/src/skill-defs.ts +17 -0
- package/src/skills/chant-grafana-alerting.md +49 -0
- package/src/skills/chant-grafana-provisioning.md +3 -3
- package/src/skills/chant-grafana.md +12 -6
- package/src/spec/fetch-cli.ts +3 -3
- package/src/spec/fetch-units-cli.ts +37 -0
- package/src/spec/overlay/canvas.overlay.json +142 -0
- package/src/spec/overlay/dashboard.overlay.json +378 -0
- package/src/spec/overlay/expr.overlay.json +14 -0
- package/src/spec/overlay/geomap.overlay.json +35 -0
- package/src/spec/overlay/logs.overlay.json +14 -0
- package/src/spec/overlay/nodegraph.overlay.json +31 -0
- package/src/spec/overlay/piechart.overlay.json +17 -0
- package/src/spec/overlay/table.overlay.json +84 -0
- package/src/spec/overlay/timeseries.overlay.json +21 -0
- package/src/spec/overlay/trend.overlay.json +17 -0
- package/src/spec/overlay/xychart.overlay.json +16 -0
- package/src/spec/overlay.ts +105 -0
- package/src/spec/schemas/barchart.jsonschema.json +419 -0
- package/src/spec/schemas/bargauge.jsonschema.json +224 -0
- package/src/spec/schemas/canvas.jsonschema.json +356 -0
- package/src/spec/schemas/dashboardv2.jsonschema.json +3387 -0
- package/src/spec/schemas/expr.jsonschema.json +987 -0
- package/src/spec/schemas/geomap.jsonschema.json +220 -0
- package/src/spec/schemas/histogram.jsonschema.json +308 -0
- package/src/spec/schemas/nodegraph.jsonschema.json +77 -0
- package/src/spec/schemas/piechart.jsonschema.json +275 -0
- package/src/spec/schemas/statetimeline.jsonschema.json +324 -0
- package/src/spec/schemas/statushistory.jsonschema.json +292 -0
- package/src/spec/schemas/trend.jsonschema.json +449 -0
- package/src/spec/schemas/xychart.jsonschema.json +428 -0
- package/src/spec/schemas.ts +22 -10
- package/src/spec/units.gen.ts +281 -0
- package/src/spec/units.ts +77 -0
- package/src/transformations.test.ts +65 -0
- package/src/transformations.ts +574 -0
- package/src/util.ts +8 -0
- package/src/validate-alerting.test.ts +255 -0
- package/src/validate-alerting.ts +428 -0
- package/src/validate-output.ts +332 -101
- package/src/validate.ts +14 -0
- package/src/variables.ts +7 -2
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The correction overlay (chant #2939, #2971): real Grafana 12.4 and 13.x
|
|
3
|
+
* exports pass GRAF107, the reproductions from the issues type and build,
|
|
4
|
+
* unknown keys are warnings, genuine type violations stay errors, and the
|
|
5
|
+
* library panels an external export embeds in `__elements` are checked.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { readdirSync, readFileSync } from "fs";
|
|
9
|
+
import { dirname, join } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { SCHEMA_NAMES } from "./pin";
|
|
12
|
+
import { applyOverlay, loadOverlay, type SchemaOverlay } from "./spec/overlay";
|
|
13
|
+
import { loadSchema, loadVendoredSchema } from "./spec/schemas";
|
|
14
|
+
import { validateDashboardSchema } from "./schema-validate";
|
|
15
|
+
import { checkSchema } from "./validate-output";
|
|
16
|
+
import { renderDashboard } from "./build";
|
|
17
|
+
import { Dashboard } from "./dashboard";
|
|
18
|
+
import { TablePanel, TimeSeriesPanel } from "./panels";
|
|
19
|
+
import { PromQuery } from "./query";
|
|
20
|
+
import { Datasource } from "./datasource";
|
|
21
|
+
|
|
22
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
23
|
+
const exportsDir = join(pkgDir, "test", "fixtures", "exports");
|
|
24
|
+
|
|
25
|
+
type Json = Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
function readJson(path: string): Json {
|
|
28
|
+
return JSON.parse(readFileSync(path, "utf-8")) as Json;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const versions = readdirSync(exportsDir).filter((d) => d.startsWith("grafana-"));
|
|
32
|
+
const classicExports = versions.flatMap((v) =>
|
|
33
|
+
readdirSync(join(exportsDir, v))
|
|
34
|
+
// The v1 and v2 resources (#2947) are API reads, not classic exports.
|
|
35
|
+
.filter((f) => f.endsWith(".json") && !f.includes("-resource"))
|
|
36
|
+
.map((f) => [`${v}/${f}`, join(exportsDir, v, f)] as const),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
describe("the overlay files", () => {
|
|
40
|
+
test("every patch cites its Grafana source and says why", () => {
|
|
41
|
+
for (const name of SCHEMA_NAMES) {
|
|
42
|
+
const overlay = loadOverlay(name);
|
|
43
|
+
if (!overlay) continue;
|
|
44
|
+
expect(overlay.grafana).toMatch(/^v\d+\.\d+\.\d+$/);
|
|
45
|
+
for (const p of overlay.patches) {
|
|
46
|
+
expect({ path: p.path, source: p.source }).toEqual({ path: p.path, source: expect.stringMatching(/\.(cue|ts|go):\d+/) });
|
|
47
|
+
expect(p.why.length).toBeGreaterThan(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the schemas with an overlay are patched; the vendored bytes are not", () => {
|
|
53
|
+
expect(SCHEMA_NAMES.filter((n) => loadOverlay(n))).toEqual(["dashboard", "timeseries", "table", "logs", "piechart", "nodegraph", "xychart", "trend", "canvas", "geomap", "expr"]);
|
|
54
|
+
const vendored = loadVendoredSchema("dashboard").definitions as Record<string, Json>;
|
|
55
|
+
const patched = loadSchema("dashboard").definitions as Record<string, Json>;
|
|
56
|
+
expect((vendored.MatcherConfig.properties as Json).scope).toBeUndefined();
|
|
57
|
+
expect((patched.MatcherConfig.properties as Json).scope).toEqual(expect.objectContaining({ $ref: "#/definitions/MatcherScope" }));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("applying refuses a patch the vendored schema already carries, or a path it lacks", () => {
|
|
61
|
+
const base = { definitions: { A: { properties: { x: { type: "string" } } } } };
|
|
62
|
+
const overlay = (patches: SchemaOverlay["patches"]): SchemaOverlay => ({ schema: "dashboard", grafana: "v13.2.2", patches });
|
|
63
|
+
const cite = { source: "kinds/dashboard/dashboard_kind.cue:1", why: "test" };
|
|
64
|
+
expect(() => applyOverlay(base, overlay([{ op: "add", path: "/definitions/A/properties/x", value: {}, ...cite }]))).toThrow(/already present/);
|
|
65
|
+
expect(() => applyOverlay(base, overlay([{ op: "replace", path: "/definitions/A/properties/y", value: {}, ...cite }]))).toThrow(/not present/);
|
|
66
|
+
expect(() => applyOverlay(base, overlay([{ op: "remove", path: "/definitions/B/properties", ...cite }]))).toThrow(/no parent/);
|
|
67
|
+
const out = applyOverlay(base, overlay([{ op: "add", path: "/definitions/A/properties/y", value: { type: "number" }, ...cite }]));
|
|
68
|
+
expect(out).toEqual({ definitions: { A: { properties: { x: { type: "string" }, y: { type: "number" } } } } });
|
|
69
|
+
expect(base.definitions.A.properties).toEqual({ x: { type: "string" } });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("real Grafana exports", () => {
|
|
74
|
+
test("the corpus covers Grafana 12.4 and 13.2, plain and shared externally", () => {
|
|
75
|
+
expect(versions).toEqual(["grafana-12.4.11", "grafana-13.2.2"]);
|
|
76
|
+
expect(classicExports.map(([name]) => name)).toEqual(
|
|
77
|
+
expect.arrayContaining([
|
|
78
|
+
"grafana-12.4.11/checkout.external.json",
|
|
79
|
+
"grafana-13.2.2/slo.external.json",
|
|
80
|
+
"grafana-13.2.2/checkout.json",
|
|
81
|
+
"grafana-13.2.2/cells.external.json",
|
|
82
|
+
]),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test.each(classicExports)("%s passes GRAF107 with no errors or warnings", (name, path) => {
|
|
87
|
+
const json = readJson(path);
|
|
88
|
+
expect({ name, problems: validateDashboardSchema(json) }).toEqual({ name, problems: [] });
|
|
89
|
+
expect(checkSchema({ dashboards: [{ source: name, json }], datasources: [] })).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("the external exports carry the metadata the overlay allows", () => {
|
|
93
|
+
const json = readJson(join(exportsDir, "grafana-13.2.2", "slo.external.json"));
|
|
94
|
+
expect(Object.keys(json)).toEqual(expect.arrayContaining(["__inputs", "__requires", "__elements"]));
|
|
95
|
+
expect(Object.keys(json.__elements as Json)).toEqual(["chant-fx-burn"]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("enum values newer than the pin", () => {
|
|
100
|
+
const cells = readJson(join(exportsDir, "grafana-13.2.2", "cells.external.json"));
|
|
101
|
+
const panels = (cells.panels as Json[]).filter((p) => !p.libraryPanel);
|
|
102
|
+
const custom = (p: Json) => ((p.fieldConfig as Json).defaults as Json).custom as Json;
|
|
103
|
+
const colorMode = (p: Json) => (((p.fieldConfig as Json).defaults as Json).color as Json).mode;
|
|
104
|
+
|
|
105
|
+
test("the 13.2 cells export uses them, in panels and in its library panel", () => {
|
|
106
|
+
expect(panels.map((p) => (custom(p)?.cellOptions as Json | undefined)?.type).filter(Boolean)).toEqual(["pill", "markdown", "geo"]);
|
|
107
|
+
expect(panels.map(colorMode)).toEqual([
|
|
108
|
+
"continuous-viridis",
|
|
109
|
+
"continuous-magma",
|
|
110
|
+
"continuous-plasma",
|
|
111
|
+
"continuous-inferno",
|
|
112
|
+
]);
|
|
113
|
+
expect(custom(panels[3]).lineStyle).toEqual({ fill: "accessible" });
|
|
114
|
+
const model = ((cells.__elements as Json)["chant-fx-owners"] as Json).model as Json;
|
|
115
|
+
expect([(custom(model).cellOptions as Json).type, colorMode(model)]).toEqual(["markdown", "continuous-cividis"]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("the vendored schemas reject them; the overlay accepts them", () => {
|
|
119
|
+
const enumOf = (schema: Json, def: string) => ((schema.definitions as Json)[def] as Json).enum as string[];
|
|
120
|
+
const fill = (schema: Json) => ((((schema.definitions as Json).LineStyle as Json).properties as Json).fill as Json).enum as string[];
|
|
121
|
+
for (const [name, def, value] of [
|
|
122
|
+
["dashboard", "FieldColorModeId", "continuous-viridis"],
|
|
123
|
+
["table", "TableCellDisplayMode", "pill"],
|
|
124
|
+
["table", "TableCellDisplayMode", "markdown"],
|
|
125
|
+
["table", "TableCellDisplayMode", "geo"],
|
|
126
|
+
] as const) {
|
|
127
|
+
expect(enumOf(loadVendoredSchema(name), def)).not.toContain(value);
|
|
128
|
+
expect(enumOf(loadSchema(name), def)).toContain(value);
|
|
129
|
+
}
|
|
130
|
+
for (const name of ["timeseries", "table"] as const) {
|
|
131
|
+
expect(fill(loadVendoredSchema(name))).not.toContain("accessible");
|
|
132
|
+
expect(fill(loadSchema(name))).toContain("accessible");
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("an unknown enum value is still an error", () => {
|
|
137
|
+
const bad = structuredClone(cells);
|
|
138
|
+
custom((bad.panels as Json[])[0]).cellOptions = { type: "hologram" };
|
|
139
|
+
const problems = validateDashboardSchema(bad);
|
|
140
|
+
expect(problems.map((p) => [p.severity, p.path])).toContainEqual(["error", "/panels/0/fieldConfig/defaults/custom/cellOptions/type"]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("library panels in __elements", () => {
|
|
145
|
+
const external = () => readJson(join(exportsDir, "grafana-13.2.2", "slo.external.json"));
|
|
146
|
+
const burn = (json: Json) => ((json.__elements as Json)["chant-fx-burn"] as Json).model as Json;
|
|
147
|
+
|
|
148
|
+
test("a bad library panel model is reported at its path", () => {
|
|
149
|
+
const json = external();
|
|
150
|
+
const model = burn(json);
|
|
151
|
+
((model.options as Json).legend as Json).displayMode = "sideways";
|
|
152
|
+
(model.targets as Json[])[0].format = "csv";
|
|
153
|
+
model.transparent = "yes";
|
|
154
|
+
model.fancyNewField = true;
|
|
155
|
+
const problems = validateDashboardSchema(json).map((p) => `${p.severity} ${p.path}: ${p.message}`);
|
|
156
|
+
expect(problems).toEqual(
|
|
157
|
+
expect.arrayContaining([
|
|
158
|
+
expect.stringMatching(/^error \/__elements\/chant-fx-burn\/model\/options\/legend\/displayMode: must be equal to one of the allowed values/),
|
|
159
|
+
expect.stringMatching(/^error \/__elements\/chant-fx-burn\/model\/targets\/0\/format: must be equal to one of the allowed values/),
|
|
160
|
+
"error /__elements/chant-fx-burn/model/transparent: must be boolean",
|
|
161
|
+
'warning /__elements/chant-fx-burn/model: unknown key "fancyNewField" (not in the pinned schema)',
|
|
162
|
+
]),
|
|
163
|
+
);
|
|
164
|
+
expect(problems).toHaveLength(4);
|
|
165
|
+
const issues = checkSchema({ dashboards: [{ source: "slo.external.json", json }], datasources: [] });
|
|
166
|
+
expect(issues.filter((i) => i.code === "GRAF107" && i.severity === "error")).toHaveLength(3);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a model with neither a type nor a library panel reference is an error", () => {
|
|
170
|
+
const json = external();
|
|
171
|
+
delete burn(json).type;
|
|
172
|
+
expect(validateDashboardSchema(json).map((p) => [p.severity, p.path, p.message])).toEqual([
|
|
173
|
+
["error", "/__elements/chant-fx-burn/model", "must have required property 'type'"],
|
|
174
|
+
["error", "/__elements/chant-fx-burn/model", "must have required property 'libraryPanel'"],
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("the element itself is checked by the envelope, and a uid with a slash is escaped in the path", () => {
|
|
179
|
+
const json = external();
|
|
180
|
+
const el = (json.__elements as Json)["chant-fx-burn"] as Json;
|
|
181
|
+
json.__elements = { "team/burn": { ...el, uid: "team/burn", name: 7, model: { ...(el.model as Json), title: 9 } } };
|
|
182
|
+
expect(validateDashboardSchema(json).map((p) => `${p.path}: ${p.message}`)).toEqual([
|
|
183
|
+
"/__elements/team~1burn/name: must be string",
|
|
184
|
+
"/__elements/team~1burn/model/title: must be string",
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("an element of another kind is not read as a panel", () => {
|
|
189
|
+
const json = external();
|
|
190
|
+
const el = (json.__elements as Json)["chant-fx-burn"] as Json;
|
|
191
|
+
json.__elements = { "chant-fx-burn": { ...el, kind: 2, model: { type: 7 } } };
|
|
192
|
+
expect(validateDashboardSchema(json)).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("the issue's reproductions", () => {
|
|
197
|
+
const prometheus = new Datasource({ name: "Prometheus", type: "prometheus" });
|
|
198
|
+
const rate = new PromQuery({ expr: "sum(rate(http_requests_total[5m]))" });
|
|
199
|
+
|
|
200
|
+
test("a byName matcher with a string option and a data link with only title and url type, build and pass", () => {
|
|
201
|
+
const panel = new TimeSeriesPanel({
|
|
202
|
+
title: "Requests",
|
|
203
|
+
datasource: prometheus,
|
|
204
|
+
targets: [rate],
|
|
205
|
+
fieldConfig: {
|
|
206
|
+
defaults: { links: [{ title: "Runbook", url: "https://example.com/runbook" }] },
|
|
207
|
+
overrides: [
|
|
208
|
+
{ matcher: { id: "byName", options: "requests" }, properties: [{ id: "unit", value: "reqps" }] },
|
|
209
|
+
{ matcher: { id: "byType", options: "number", scope: "series" }, properties: [{ id: "decimals", value: 1 }] },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
links: [{ title: "Traces", url: "https://example.com/traces", targetBlank: true }],
|
|
213
|
+
});
|
|
214
|
+
const json = renderDashboard(new Dashboard({ title: "Repro", uid: "repro", panels: [panel] })) as unknown as Json;
|
|
215
|
+
expect(validateDashboardSchema(json)).toEqual([]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a table cellOptions matching several variants is accepted", () => {
|
|
219
|
+
const panel = new TablePanel({ title: "T", fieldConfig: { defaults: { custom: { cellOptions: { type: "auto" } } } } });
|
|
220
|
+
const json = renderDashboard(new Dashboard({ title: "Table", uid: "table", panels: [panel] })) as unknown as Json;
|
|
221
|
+
expect(validateDashboardSchema(json)).toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("errors and warnings", () => {
|
|
226
|
+
function dashboard(extra: Json = {}, panel: Json = {}): Json {
|
|
227
|
+
return {
|
|
228
|
+
uid: "d",
|
|
229
|
+
title: "D",
|
|
230
|
+
schemaVersion: 42,
|
|
231
|
+
panels: [{ type: "timeseries", id: 1, gridPos: { x: 0, y: 0, w: 12, h: 8 }, fieldConfig: { defaults: {}, overrides: [] }, ...panel }],
|
|
232
|
+
...extra,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
test("a key the pin does not know is a warning, wherever it is", () => {
|
|
237
|
+
const problems = validateDashboardSchema(
|
|
238
|
+
dashboard({ fancyNewField: true }, { fieldConfig: { defaults: { custom: { brandNewOption: 1 } }, overrides: [] } }),
|
|
239
|
+
);
|
|
240
|
+
expect(problems).toEqual([
|
|
241
|
+
{ path: "/", message: 'unknown key "fancyNewField" (not in the pinned schema)', severity: "warning" },
|
|
242
|
+
{ path: "/panels/0/fieldConfig/defaults/custom", message: 'unknown key "brandNewOption" (not in the pinned schema)', severity: "warning" },
|
|
243
|
+
]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("a genuine type violation stays an error", () => {
|
|
247
|
+
const problems = validateDashboardSchema(
|
|
248
|
+
dashboard(
|
|
249
|
+
{ templating: { list: [{ type: "query", name: "q", query: 5 }] } },
|
|
250
|
+
{ fieldConfig: { defaults: {}, overrides: [{ matcher: { id: 7, scope: "everything" }, properties: [] }] }, links: [{ url: "x" }] },
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
expect(problems.filter((p) => p.severity === "error").map((p) => `${p.path}: ${p.message}`)).toEqual(
|
|
254
|
+
expect.arrayContaining([
|
|
255
|
+
"/templating/list/0/query: must be string",
|
|
256
|
+
"/templating/list/0/query: must be object",
|
|
257
|
+
"/panels/0/links/0: must have required property 'title'",
|
|
258
|
+
"/panels/0/fieldConfig/overrides/0/matcher/id: must be string",
|
|
259
|
+
expect.stringMatching(/^\/panels\/0\/fieldConfig\/overrides\/0\/matcher\/scope: must be equal to one of the allowed values/),
|
|
260
|
+
]),
|
|
261
|
+
);
|
|
262
|
+
expect(problems.every((p) => p.severity === "error")).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("a panel with neither a type nor a library panel is an error; a library panel reference is fine", () => {
|
|
266
|
+
const bare = validateDashboardSchema({ uid: "d", title: "D", schemaVersion: 42, panels: [{ id: 1, gridPos: { x: 0, y: 0, w: 6, h: 6 } }] });
|
|
267
|
+
expect(bare.map((p) => [p.severity, p.message])).toEqual([
|
|
268
|
+
["error", "must have required property 'type'"],
|
|
269
|
+
["error", "must have required property 'libraryPanel'"],
|
|
270
|
+
]);
|
|
271
|
+
const lib = { id: 1, gridPos: { x: 0, y: 0, w: 6, h: 6 }, libraryPanel: { uid: "u", name: "n" } };
|
|
272
|
+
expect(validateDashboardSchema({ uid: "d", title: "D", schemaVersion: 42, panels: [lib] })).toEqual([]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("GRAF107 carries the severity through", () => {
|
|
276
|
+
const issues = checkSchema({ dashboards: [{ json: dashboard({ fancyNewField: true, graphTooltip: 7 }) }], datasources: [] });
|
|
277
|
+
expect(issues.map((i) => [i.code, i.severity])).toEqual([
|
|
278
|
+
["GRAF107", "error"],
|
|
279
|
+
["GRAF107", "warning"],
|
|
280
|
+
]);
|
|
281
|
+
});
|
|
282
|
+
});
|
package/src/schema-validate.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validate built Grafana JSON against the vendored schemas at
|
|
3
|
-
* `GRAFANA_SCHEMA_PIN`, offline.
|
|
3
|
+
* `GRAFANA_SCHEMA_PIN`, with the correction overlay applied, offline.
|
|
4
4
|
*
|
|
5
5
|
* The dashboard is checked against the dashboard schema as it stands. Panel
|
|
6
6
|
* options, `fieldConfig.defaults.custom` and each query are checked against
|
|
7
7
|
* their plugin's schema with every `required` list removed, because Grafana
|
|
8
|
-
* fills in options a panel leaves out
|
|
9
|
-
*
|
|
8
|
+
* fills in options a panel leaves out.
|
|
9
|
+
*
|
|
10
|
+
* Two kinds of problem come back. A value the schema does not allow (a
|
|
11
|
+
* wrong type, an enum value it does not list, a required field missing) is
|
|
12
|
+
* an error. A key the pinned schema does not know is a warning: Grafana
|
|
13
|
+
* adds fields every minor release and keeps what it does not understand, so
|
|
14
|
+
* an unknown key is more often a newer Grafana than a mistake. To tell the
|
|
15
|
+
* two apart each value is validated twice, once with every object opened
|
|
16
|
+
* (`additionalProperties: false` dropped; what fails is an error) and once
|
|
17
|
+
* as pinned (what fails only for an unknown key is a warning).
|
|
18
|
+
*
|
|
19
|
+
* An export shared externally embeds its library panels in `__elements`;
|
|
20
|
+
* each element's `model` is checked the same way as a panel on the grid.
|
|
21
|
+
*
|
|
22
|
+
* `oneOf` is read as `anyOf` throughout. The CUE these schemas come from
|
|
23
|
+
* has disjunctions, which accept a value matching any branch; cog writes
|
|
24
|
+
* them as `oneOf`, which rejects a value matching two, and loosely
|
|
25
|
+
* discriminated unions (a table's `cellOptions: { type: "auto" }` matches
|
|
26
|
+
* several variants) match more than one.
|
|
10
27
|
*/
|
|
11
28
|
|
|
12
29
|
import Ajv, { type ValidateFunction, type ErrorObject } from "ajv";
|
|
@@ -19,6 +36,8 @@ export interface SchemaProblem {
|
|
|
19
36
|
/** JSON pointer into the dashboard. */
|
|
20
37
|
path: string;
|
|
21
38
|
message: string;
|
|
39
|
+
/** `error` for a value the schema does not allow, `warning` for a key it does not know. */
|
|
40
|
+
severity: "error" | "warning";
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
let ajv: Ajv | undefined;
|
|
@@ -29,22 +48,30 @@ function getAjv(): Ajv {
|
|
|
29
48
|
|
|
30
49
|
const compiled = new Map<string, ValidateFunction>();
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
interface Mode {
|
|
52
|
+
/** Drop every `required` list (panel options and queries: Grafana fills them in). */
|
|
53
|
+
lenient: boolean;
|
|
54
|
+
/** Drop every `additionalProperties: false`, so unknown keys pass. */
|
|
55
|
+
open: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function prepare(node: unknown, mode: Mode): unknown {
|
|
59
|
+
if (Array.isArray(node)) return node.map((n) => prepare(n, mode));
|
|
34
60
|
if (node && typeof node === "object") {
|
|
35
61
|
const out: Record<string, unknown> = {};
|
|
36
62
|
for (const [k, v] of Object.entries(node)) {
|
|
37
|
-
if (k === "required" && Array.isArray(v)) continue;
|
|
38
|
-
|
|
63
|
+
if (mode.lenient && k === "required" && Array.isArray(v)) continue;
|
|
64
|
+
if (mode.open && k === "additionalProperties" && v === false) continue;
|
|
65
|
+
out[k === "oneOf" && Array.isArray(v) ? "anyOf" : k] = prepare(v, mode);
|
|
39
66
|
}
|
|
40
67
|
return out;
|
|
41
68
|
}
|
|
42
69
|
return node;
|
|
43
70
|
}
|
|
44
71
|
|
|
45
|
-
/** A validator for one definition of one
|
|
46
|
-
function validatorFor(name: SchemaName, def: string | undefined,
|
|
47
|
-
const key = `${name}#${def ?? ""}#${lenient}`;
|
|
72
|
+
/** A validator for one definition of one schema (the root `$ref` when `def` is left out). */
|
|
73
|
+
function validatorFor(name: SchemaName, def: string | undefined, mode: Mode): ValidateFunction | undefined {
|
|
74
|
+
const key = `${name}#${def ?? ""}#${mode.lenient}#${mode.open}`;
|
|
48
75
|
const hit = compiled.get(key);
|
|
49
76
|
if (hit) return hit;
|
|
50
77
|
const schema = loadSchema(name);
|
|
@@ -55,31 +82,48 @@ function validatorFor(name: SchemaName, def: string | undefined, lenient: boolea
|
|
|
55
82
|
const root: Record<string, unknown> = {
|
|
56
83
|
$schema: schema.$schema,
|
|
57
84
|
$ref: ref,
|
|
58
|
-
definitions:
|
|
85
|
+
definitions: prepare(definitions, mode),
|
|
59
86
|
};
|
|
60
87
|
const fn = getAjv().compile(root);
|
|
61
88
|
compiled.set(key, fn);
|
|
62
89
|
return fn;
|
|
63
90
|
}
|
|
64
91
|
|
|
65
|
-
function
|
|
92
|
+
function describe(e: ErrorObject): string {
|
|
93
|
+
const extra =
|
|
94
|
+
e.keyword === "enum" ? `: ${(e.params as { allowedValues: unknown[] }).allowedValues.map((v) => JSON.stringify(v)).join(", ")}` : "";
|
|
95
|
+
return `${e.message ?? "is invalid"}${extra}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Everything wrong with one value against one definition: errors from the
|
|
100
|
+
* opened schema, then unknown keys from the pinned one.
|
|
101
|
+
*/
|
|
102
|
+
function check(value: unknown, name: SchemaName, def: string | undefined, lenient: boolean, prefix: string): SchemaProblem[] {
|
|
103
|
+
const open = validatorFor(name, def, { lenient, open: true });
|
|
104
|
+
const closed = validatorFor(name, def, { lenient, open: false });
|
|
105
|
+
if (!open || !closed) return [];
|
|
66
106
|
const seen = new Set<string>();
|
|
67
107
|
const out: SchemaProblem[] = [];
|
|
68
|
-
|
|
69
|
-
// oneOf/anyOf wrappers repeat what their branches already said.
|
|
70
|
-
if (e.keyword === "oneOf" || e.keyword === "anyOf" || e.keyword === "if") continue;
|
|
71
|
-
const extra =
|
|
72
|
-
e.keyword === "additionalProperties"
|
|
73
|
-
? ` ("${(e.params as { additionalProperty: string }).additionalProperty}")`
|
|
74
|
-
: e.keyword === "enum"
|
|
75
|
-
? `: ${(e.params as { allowedValues: unknown[] }).allowedValues.map((v) => JSON.stringify(v)).join(", ")}`
|
|
76
|
-
: "";
|
|
77
|
-
const path = `${prefix}${e.instancePath}` || "/";
|
|
78
|
-
const message = `${e.message ?? "is invalid"}${extra}`;
|
|
108
|
+
const push = (path: string, message: string, severity: SchemaProblem["severity"]) => {
|
|
79
109
|
const k = `${path} ${message}`;
|
|
80
|
-
if (seen.has(k))
|
|
110
|
+
if (seen.has(k)) return;
|
|
81
111
|
seen.add(k);
|
|
82
|
-
out.push({ path, message });
|
|
112
|
+
out.push({ path, message, severity });
|
|
113
|
+
};
|
|
114
|
+
if (!open(value)) {
|
|
115
|
+
for (const e of open.errors ?? []) {
|
|
116
|
+
// anyOf/if wrappers repeat what their branches already said.
|
|
117
|
+
if (e.keyword === "oneOf" || e.keyword === "anyOf" || e.keyword === "if") continue;
|
|
118
|
+
push(`${prefix}${e.instancePath}` || "/", describe(e), "error");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!closed(value)) {
|
|
122
|
+
for (const e of closed.errors ?? []) {
|
|
123
|
+
if (e.keyword !== "additionalProperties") continue;
|
|
124
|
+
const key = (e.params as { additionalProperty: string }).additionalProperty;
|
|
125
|
+
push(`${prefix}${e.instancePath}` || "/", `unknown key "${key}" (not in the pinned schema)`, "warning");
|
|
126
|
+
}
|
|
83
127
|
}
|
|
84
128
|
return out;
|
|
85
129
|
}
|
|
@@ -98,46 +142,93 @@ function panelsOf(dashboard: Json): Array<{ panel: Json; path: string }> {
|
|
|
98
142
|
return out;
|
|
99
143
|
}
|
|
100
144
|
|
|
101
|
-
/**
|
|
145
|
+
/** A panel (not a row) against the dashboard's Panel definition and its plugin's options, custom field config and queries. */
|
|
146
|
+
function checkPanel(panel: Json, path: string): SchemaProblem[] {
|
|
147
|
+
const out = check(panel, "dashboard", "Panel", false, path);
|
|
148
|
+
const def = typeof panel.type === "string" ? panelDefinitionFor(panel.type) : undefined;
|
|
149
|
+
if (def?.schema) {
|
|
150
|
+
if (panel.options !== undefined) out.push(...check(panel.options, def.schema, "Options", true, `${path}/options`));
|
|
151
|
+
const custom = (panel.fieldConfig as { defaults?: { custom?: unknown } } | undefined)?.defaults?.custom;
|
|
152
|
+
if (custom !== undefined) out.push(...check(custom, def.schema, "FieldConfig", true, `${path}/fieldConfig/defaults/custom`));
|
|
153
|
+
}
|
|
154
|
+
const panelType = (panel.datasource as { type?: string } | undefined)?.type;
|
|
155
|
+
const targets = Array.isArray(panel.targets) ? (panel.targets as Json[]) : [];
|
|
156
|
+
targets.forEach((t, k) => {
|
|
157
|
+
const type = (t?.datasource as { type?: string } | undefined)?.type ?? panelType;
|
|
158
|
+
const qdef = type ? queryDefinitionFor(type) : undefined;
|
|
159
|
+
if (!qdef?.schema) return;
|
|
160
|
+
out.push(...check(t, qdef.schema, undefined, true, `${path}/targets/${k}`));
|
|
161
|
+
});
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** `LibraryElementKind.Panel` (public/app/features/library-panels/types.ts:8-10 at v13.2.2, its only member). */
|
|
166
|
+
const LIBRARY_PANEL_KIND = 1;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The library panel models an external export embeds in `__elements`. The
|
|
170
|
+
* envelope check covers each element's own fields (uid, name, kind, model
|
|
171
|
+
* present); the model is a whole panel, checked like one.
|
|
172
|
+
*/
|
|
173
|
+
function libraryPanelModels(dashboard: Json): Array<{ panel: Json; path: string }> {
|
|
174
|
+
const elements = dashboard.__elements;
|
|
175
|
+
if (!elements || typeof elements !== "object" || Array.isArray(elements)) return [];
|
|
176
|
+
const out: Array<{ panel: Json; path: string }> = [];
|
|
177
|
+
for (const [key, el] of Object.entries(elements as Record<string, unknown>)) {
|
|
178
|
+
if (!el || typeof el !== "object") continue;
|
|
179
|
+
const { kind, model } = el as { kind?: unknown; model?: unknown };
|
|
180
|
+
if (kind !== undefined && kind !== LIBRARY_PANEL_KIND) continue;
|
|
181
|
+
if (!model || typeof model !== "object" || Array.isArray(model)) continue;
|
|
182
|
+
out.push({ panel: model as Json, path: `/__elements/${key.replace(/~/g, "~0").replace(/\//g, "~1")}/model` });
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Everything in one dashboard that the pinned schemas reject or do not know. */
|
|
102
188
|
export function validateDashboardSchema(dashboard: Json): SchemaProblem[] {
|
|
103
189
|
const out: SchemaProblem[] = [];
|
|
104
190
|
// The envelope and each panel are checked separately: the schema's
|
|
105
191
|
// `oneOf [Panel, RowPanel]` would otherwise report every panel error twice,
|
|
106
192
|
// once per branch.
|
|
107
|
-
const whole = validatorFor("dashboard", undefined, false)!;
|
|
108
193
|
const envelope = Array.isArray(dashboard.panels) ? { ...dashboard, panels: [] } : dashboard;
|
|
109
|
-
|
|
110
|
-
const panelSchema = validatorFor("dashboard", "Panel", false)!;
|
|
111
|
-
const rowSchema = validatorFor("dashboard", "RowPanel", false)!;
|
|
194
|
+
out.push(...check(envelope, "dashboard", undefined, false, ""));
|
|
112
195
|
|
|
113
196
|
for (const { panel, path } of panelsOf(dashboard)) {
|
|
114
197
|
if (!panel || typeof panel !== "object") {
|
|
115
|
-
out.push({ path, message: "must be object" });
|
|
198
|
+
out.push({ path, message: "must be object", severity: "error" });
|
|
116
199
|
continue;
|
|
117
200
|
}
|
|
118
201
|
if (panel.type === "row") {
|
|
119
202
|
const header = Array.isArray(panel.panels) ? { ...panel, panels: [] } : panel;
|
|
120
|
-
|
|
203
|
+
out.push(...check(header, "dashboard", "RowPanel", false, path));
|
|
121
204
|
continue;
|
|
122
205
|
}
|
|
123
|
-
|
|
124
|
-
const def = typeof panel.type === "string" ? panelDefinitionFor(panel.type) : undefined;
|
|
125
|
-
if (def?.schema) {
|
|
126
|
-
const options = validatorFor(def.schema, "Options", true);
|
|
127
|
-
if (options && panel.options !== undefined && !options(panel.options)) out.push(...problems(options.errors, `${path}/options`));
|
|
128
|
-
const custom = (panel.fieldConfig as { defaults?: { custom?: unknown } } | undefined)?.defaults?.custom;
|
|
129
|
-
const fc = validatorFor(def.schema, "FieldConfig", true);
|
|
130
|
-
if (fc && custom !== undefined && !fc(custom)) out.push(...problems(fc.errors, `${path}/fieldConfig/defaults/custom`));
|
|
131
|
-
}
|
|
132
|
-
const panelType = (panel.datasource as { type?: string } | undefined)?.type;
|
|
133
|
-
const targets = Array.isArray(panel.targets) ? (panel.targets as Json[]) : [];
|
|
134
|
-
targets.forEach((t, k) => {
|
|
135
|
-
const type = (t?.datasource as { type?: string } | undefined)?.type ?? panelType;
|
|
136
|
-
const qdef = type ? queryDefinitionFor(type) : undefined;
|
|
137
|
-
if (!qdef?.schema) return;
|
|
138
|
-
const v = validatorFor(qdef.schema, undefined, true);
|
|
139
|
-
if (v && !v(t)) out.push(...problems(v.errors, `${path}/targets/${k}`));
|
|
140
|
-
});
|
|
206
|
+
out.push(...checkPanel(panel, path));
|
|
141
207
|
}
|
|
208
|
+
for (const { panel, path } of libraryPanelModels(dashboard)) out.push(...checkPanel(panel, path));
|
|
142
209
|
return out;
|
|
143
210
|
}
|
|
211
|
+
|
|
212
|
+
/** The `expr` schema definition for each server-side expression type. */
|
|
213
|
+
const EXPRESSION_DEFINITIONS: Record<string, string> = {
|
|
214
|
+
math: "TypeMath",
|
|
215
|
+
reduce: "TypeReduce",
|
|
216
|
+
resample: "TypeResample",
|
|
217
|
+
classic_conditions: "TypeClassicConditions",
|
|
218
|
+
threshold: "TypeThreshold",
|
|
219
|
+
sql: "TypeSql",
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check one server-side expression model (an alert rule's `data[].model`
|
|
224
|
+
* whose datasource is `__expr__`) against the pinned `expr` schema, as
|
|
225
|
+
* written: its `type` picks the definition, and every required field must
|
|
226
|
+
* be there, since Grafana fills none in.
|
|
227
|
+
*/
|
|
228
|
+
export function validateExpressionSchema(model: Json, prefix = ""): SchemaProblem[] {
|
|
229
|
+
const def = typeof model.type === "string" ? EXPRESSION_DEFINITIONS[model.type] : undefined;
|
|
230
|
+
if (!def) {
|
|
231
|
+
return [{ path: `${prefix}/type`, message: `is ${JSON.stringify(model.type)}, not one of ${Object.keys(EXPRESSION_DEFINITIONS).join(", ")}`, severity: "error" }];
|
|
232
|
+
}
|
|
233
|
+
return check(model, "expr", def, false, prefix);
|
|
234
|
+
}
|
package/src/schema.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest";
|
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { GRAFANA_SCHEMA_PIN, SCHEMA_NAMES, schemaUrl } from "./pin";
|
|
5
|
+
import { GRAFANA_SCHEMA_PIN, SCHEMA_NAMES, VENDORED_SCHEMA_NAMES, schemaUrl } from "./pin";
|
|
6
6
|
import { digestMismatches, loadSchema } from "./spec/schemas";
|
|
7
7
|
import { schemaModules } from "./codegen/generate";
|
|
8
8
|
import { schemaModule, tsType } from "./codegen/schema-types";
|
|
@@ -13,7 +13,9 @@ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
|
13
13
|
describe("the schema pin", () => {
|
|
14
14
|
test("names a commit and a digest for every vendored schema", () => {
|
|
15
15
|
expect(GRAFANA_SCHEMA_PIN.commit).toMatch(/^[0-9a-f]{40}$/);
|
|
16
|
-
expect(Object.keys(GRAFANA_SCHEMA_PIN.files).sort()).toEqual([...
|
|
16
|
+
expect(Object.keys(GRAFANA_SCHEMA_PIN.files).sort()).toEqual([...VENDORED_SCHEMA_NAMES].sort());
|
|
17
|
+
// Types come from the classic schemas only; dashboardv2 is vendored for the importer (#2947).
|
|
18
|
+
expect(VENDORED_SCHEMA_NAMES.filter((n) => !(SCHEMA_NAMES as readonly string[]).includes(n))).toEqual(["dashboardv2"]);
|
|
17
19
|
expect(schemaUrl("stat")).toBe(`https://raw.githubusercontent.com/grafana/grafana-foundation-sdk/${GRAFANA_SCHEMA_PIN.commit}/jsonschema/stat.jsonschema.json`);
|
|
18
20
|
});
|
|
19
21
|
|
package/src/serializer.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { load } from "js-yaml";
|
|
|
7
7
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
8
8
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
9
9
|
import { grafanaSerializer } from "./serializer";
|
|
10
|
-
import { Datasource } from "./datasource";
|
|
10
|
+
import { Datasource, ExternalDatasource } from "./datasource";
|
|
11
11
|
import { Dashboard, DashboardProvider } from "./dashboard";
|
|
12
12
|
import { StatPanel, TimeSeriesPanel } from "./panels";
|
|
13
13
|
import { PromQuery } from "./query";
|
|
@@ -150,6 +150,18 @@ describe("grafana serializer", () => {
|
|
|
150
150
|
expect(JSON.parse(out.primary).files).toEqual(Object.keys(out.files!).sort());
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
test("an ExternalDatasource is referenced by uid, in panels and in another datasource's settings, and not provisioned", () => {
|
|
154
|
+
const loki = new ExternalDatasource({ type: "loki", uid: "loki-prod", name: "Loki (prod)" });
|
|
155
|
+
const tempo = new Datasource({ name: "Tempo", type: "tempo", jsonData: { tracesToLogsV2: { datasourceUid: loki } } });
|
|
156
|
+
const logs = new StatPanel({ datasource: loki });
|
|
157
|
+
const out = run({ loki, tempo, d: new Dashboard({ title: "D", panels: [logs] }) });
|
|
158
|
+
expect(JSON.parse(out.files!["dashboards/d.json"]).panels[0].datasource).toEqual({ type: "loki", uid: "loki-prod" });
|
|
159
|
+
const ds = load(out.files![DATASOURCES_FILE]) as { datasources: Array<{ name: string; jsonData?: unknown }> };
|
|
160
|
+
expect(ds.datasources.map((d) => d.name)).toEqual(["Tempo"]);
|
|
161
|
+
expect(ds.datasources[0].jsonData).toEqual({ tracesToLogsV2: { datasourceUid: "loki-prod" } });
|
|
162
|
+
expect(JSON.parse(out.primary).externalDatasources).toEqual([{ type: "loki", uid: "loki-prod", name: "Loki (prod)" }]);
|
|
163
|
+
});
|
|
164
|
+
|
|
153
165
|
test("round trip: the emitted text parses back to the declared values", () => {
|
|
154
166
|
const panel = new TimeSeriesPanel({ title: "Rate", datasource: prometheus, targets: [up], interval: "30s" });
|
|
155
167
|
const out = run({ d: new Dashboard({ title: "Round trip", description: "desc", tags: ["a", "b"], panels: [panel] }) });
|
package/src/serializer.ts
CHANGED
|
@@ -9,13 +9,21 @@
|
|
|
9
9
|
* - `provisioning/datasources/chant.yaml`: every declared datasource.
|
|
10
10
|
* - `provisioning/dashboards/chant.yaml`: the provider that loads the
|
|
11
11
|
* dashboard files, a default one unless a `DashboardProvider` is declared.
|
|
12
|
+
* - `provisioning/alerting/chant.yaml`: rule groups, contact points, the
|
|
13
|
+
* notification policy tree, mute timings and templates, when the build
|
|
14
|
+
* declares any (see `alerting-build.ts`).
|
|
12
15
|
*
|
|
13
16
|
* Mount `provisioning/` at `/etc/grafana/provisioning` and `dashboards/` at
|
|
14
17
|
* the provider's path (`/var/lib/grafana/dashboards` by default).
|
|
15
18
|
*
|
|
16
|
-
* The primary output is a small JSON index of what was built.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* The primary output is a small JSON index of what was built. The dashboard
|
|
20
|
+
* JSON carries no ownership marker, because Grafana keeps who manages a
|
|
21
|
+
* dashboard outside it, in the resource's metadata on its
|
|
22
|
+
* `dashboard.grafana.app` API. For a file-provisioned dashboard that metadata
|
|
23
|
+
* is the `grafana.app/managerId` annotation Grafana writes, whose value is the
|
|
24
|
+
* provider name in `provisioning/dashboards/chant.yaml`: `chant` unless a
|
|
25
|
+
* `DashboardProvider` names another. That name is the marker chant stamps at
|
|
26
|
+
* synthesis, and ./ownership.ts reads it back.
|
|
19
27
|
*/
|
|
20
28
|
|
|
21
29
|
import type { Declarable } from "@intentius/chant/declarable";
|
package/src/skill-defs.ts
CHANGED
|
@@ -28,4 +28,21 @@ export const grafanaSkills = createSkillsLoader(import.meta.url, [
|
|
|
28
28
|
description: "Load chant-built Grafana dashboards and datasources into a running Grafana through its provisioning directories, in Docker or Kubernetes",
|
|
29
29
|
triggers: [{ type: "context" as const, value: "grafana provisioning" }],
|
|
30
30
|
},
|
|
31
|
+
{
|
|
32
|
+
file: "chant-grafana-alerting.md",
|
|
33
|
+
name: "chant-grafana-alerting",
|
|
34
|
+
description:
|
|
35
|
+
"Declare Grafana-managed alert rules, server-side expressions, contact points, notification policies and mute timings, turn an Slo into burn-rate rules, and import Grafana's alerting exports",
|
|
36
|
+
triggers: [
|
|
37
|
+
{ type: "context" as const, value: "grafana alerting" },
|
|
38
|
+
{ type: "context" as const, value: "grafana alert rule" },
|
|
39
|
+
{ type: "context" as const, value: "contact point" },
|
|
40
|
+
],
|
|
41
|
+
examples: [
|
|
42
|
+
{
|
|
43
|
+
title: "An SLO's burn-rate alerts as Grafana-managed rules",
|
|
44
|
+
output: 'export const checkoutBurn = SloAlertRules({ slo: checkout, datasource: prometheus, folder: "SLOs" });',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
31
48
|
]);
|