@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,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep observation (#2946) against an in-memory Grafana: the built examples,
|
|
3
|
+
* stored the way Grafana stores them, read back and diffed against their
|
|
4
|
+
* declarations through core's own deep diff.
|
|
5
|
+
*
|
|
6
|
+
* The first block is the noise check: an untouched dashboard must report no
|
|
7
|
+
* drift and nothing unclaimed, on the 13.x, 12.x and 11.x read paths. The
|
|
8
|
+
* rest edit the stored copy the way a UI save would and pin what the diff
|
|
9
|
+
* reports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { build } from "@intentius/chant/build";
|
|
16
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
17
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
18
|
+
import { otelSerializer } from "@intentius/chant-lexicon-otel/serializer";
|
|
19
|
+
import { prometheusSerializer } from "@intentius/chant-lexicon-prometheus/serializer";
|
|
20
|
+
import { grafanaSerializer } from "./serializer";
|
|
21
|
+
import { observeResourcesDeepGrafana } from "./deep-observe";
|
|
22
|
+
import { grafanaDeepNormalizationHooks } from "./deep-observe-hooks";
|
|
23
|
+
import { fakeGrafana, liveDatasource, storedDashboard, type FakeGrafanaState } from "./api/fake-grafana";
|
|
24
|
+
import { load } from "js-yaml";
|
|
25
|
+
import { DATASOURCES_FILE, type GrafanaIndex } from "./build";
|
|
26
|
+
|
|
27
|
+
const { diffDeepObservation } = await import("@intentius/chant/lifecycle/deep-observe");
|
|
28
|
+
const { normalizeDeepObservation } = await import("@intentius/chant/deep-observation");
|
|
29
|
+
|
|
30
|
+
type Json = Record<string, unknown>;
|
|
31
|
+
type Entities = Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
32
|
+
|
|
33
|
+
const EXAMPLES = join(import.meta.dirname, "..", "examples");
|
|
34
|
+
const CONFIG = { grafana: { profiles: { prod: { url: "http://grafana.test" } } } };
|
|
35
|
+
|
|
36
|
+
interface Built {
|
|
37
|
+
entities: Entities;
|
|
38
|
+
index: GrafanaIndex;
|
|
39
|
+
files: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function buildExample(name: string): Promise<Built> {
|
|
43
|
+
const result = await build(join(EXAMPLES, name, "src"), [otelSerializer, prometheusSerializer, grafanaSerializer]);
|
|
44
|
+
expect(result.errors).toEqual([]);
|
|
45
|
+
const out = result.outputs.get("grafana") as SerializerResult;
|
|
46
|
+
const entities: Entities = new Map();
|
|
47
|
+
for (const [n, e] of result.entities) {
|
|
48
|
+
if (e.lexicon === "grafana" && isResourceDeclarable(e)) entities.set(n, { entityType: e.entityType, props: e.props as Record<string, unknown> });
|
|
49
|
+
}
|
|
50
|
+
return { entities, index: JSON.parse(out.primary) as GrafanaIndex, files: out.files ?? {} };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Grafana after provisioning the build: every dashboard stored, in its folder, managed by the `chant` provider; every datasource. */
|
|
54
|
+
function provisioned(b: Built, api: FakeGrafanaState["api"]): FakeGrafanaState {
|
|
55
|
+
const folders: Record<string, string> = {};
|
|
56
|
+
const dashboards: FakeGrafanaState["dashboards"] = {};
|
|
57
|
+
for (const d of b.index.dashboards) {
|
|
58
|
+
const folderUid = d.folder ? `f-${d.folder.toLowerCase()}` : undefined;
|
|
59
|
+
if (folderUid) folders[folderUid] = d.folder!;
|
|
60
|
+
dashboards[d.uid] = {
|
|
61
|
+
spec: storedDashboard(JSON.parse(b.files[d.file]) as Json),
|
|
62
|
+
annotations: { "grafana.app/managedBy": "classic-file-provisioning", "grafana.app/managerId": "chant" },
|
|
63
|
+
...(folderUid ? { folderUid } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Datasources as Grafana serves them after reading the provisioning file.
|
|
67
|
+
const datasources: Record<string, Json> = {};
|
|
68
|
+
const file = b.files[DATASOURCES_FILE];
|
|
69
|
+
const entries = file ? ((load(file) as { datasources: Json[] }).datasources ?? []) : [];
|
|
70
|
+
for (const ds of entries) {
|
|
71
|
+
datasources[String(ds.uid)] = {
|
|
72
|
+
...liveDatasource({ name: String(ds.name), type: String(ds.type), uid: String(ds.uid), url: ds.url as string | undefined, isDefault: ds.isDefault as boolean | undefined, jsonData: ds.jsonData as Json | undefined, secure: Object.keys((ds.secureJsonData as Json | undefined) ?? {}) }),
|
|
73
|
+
readOnly: ds.editable !== true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { api, dashboards, datasources, folders };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function diff(b: Built, state: FakeGrafanaState) {
|
|
80
|
+
const live = await observeResourcesDeepGrafana({
|
|
81
|
+
environment: "prod",
|
|
82
|
+
entityNames: [...b.entities.keys()],
|
|
83
|
+
entities: b.entities,
|
|
84
|
+
config: CONFIG,
|
|
85
|
+
http: fakeGrafana(state),
|
|
86
|
+
});
|
|
87
|
+
return { live, result: diffDeepObservation(b.entities, normalizeDeepObservation(live), grafanaDeepNormalizationHooks) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function driftPaths(result: Awaited<ReturnType<typeof diff>>["result"]): string[] {
|
|
91
|
+
return result.drifted.flatMap((e) => e.changes.map((c) => `${e.name}:${c.kind}:${c.path}`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function unclaimedPaths(result: Awaited<ReturnType<typeof diff>>["result"]): string[] {
|
|
95
|
+
return result.unclaimed.flatMap((e) => e.fields.map((f) => `${e.name}:${f.path}`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let gettingStarted: Built;
|
|
99
|
+
let declarations: Built;
|
|
100
|
+
|
|
101
|
+
beforeAll(async () => {
|
|
102
|
+
gettingStarted = await buildExample("getting-started");
|
|
103
|
+
declarations = await buildExample("dashboards-from-declarations");
|
|
104
|
+
}, 60_000);
|
|
105
|
+
|
|
106
|
+
describe("an untouched provisioned dashboard reads back with no drift and nothing unclaimed", () => {
|
|
107
|
+
for (const api of ["v1", "v1beta1", "legacy"] as const) {
|
|
108
|
+
it(`getting-started, ${api}`, async () => {
|
|
109
|
+
const { live, result } = await diff(gettingStarted, provisioned(gettingStarted, api));
|
|
110
|
+
expect(normalizeDeepObservation(live).unobserved).toEqual({});
|
|
111
|
+
expect(driftPaths(result)).toEqual([]);
|
|
112
|
+
expect(unclaimedPaths(result)).toEqual([]);
|
|
113
|
+
expect(result.unchanged).toEqual(expect.arrayContaining(["serviceOverview", "prometheus", "tempo", "loki"]));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it(`dashboards-from-declarations (RED, SLO and agent composites), ${api}`, async () => {
|
|
117
|
+
const { result } = await diff(declarations, provisioned(declarations, api));
|
|
118
|
+
expect(driftPaths(result)).toEqual([]);
|
|
119
|
+
expect(unclaimedPaths(result)).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("an edited dashboard is drift, at the path the author wrote", () => {
|
|
125
|
+
function edited(b: Built, uid: string, edit: (spec: Json) => void): FakeGrafanaState {
|
|
126
|
+
const state = provisioned(b, "v1");
|
|
127
|
+
edit(state.dashboards[uid].spec);
|
|
128
|
+
return state;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const panels = (spec: Json) => spec.panels as Json[];
|
|
132
|
+
|
|
133
|
+
it("a query changed in the UI", async () => {
|
|
134
|
+
const state = edited(gettingStarted, "service-overview", (spec) => {
|
|
135
|
+
const p = panels(spec).find((x) => x.title === "Requests per second by operation")!;
|
|
136
|
+
(p.targets as Json[])[0].expr = "sum(rate(http_requests_total[1m]))";
|
|
137
|
+
});
|
|
138
|
+
const { result } = await diff(gettingStarted, state);
|
|
139
|
+
const changes = result.drifted.find((e) => e.name === "serviceOverview")!.changes;
|
|
140
|
+
expect(changes).toEqual([
|
|
141
|
+
expect.objectContaining({ kind: "changed", path: expect.stringMatching(/^panels\[\d+\]\.panels\[\d+\]\.targets\[0\]\.expr$/), live: "sum(rate(http_requests_total[1m]))" }),
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("a panel retitled, and the dashboard's time range and refresh changed", async () => {
|
|
146
|
+
const state = edited(gettingStarted, "service-overview", (spec) => {
|
|
147
|
+
panels(spec).find((x) => x.title === "Error ratio")!.title = "Errors";
|
|
148
|
+
spec.time = { from: "now-24h", to: "now" };
|
|
149
|
+
spec.refresh = "5m";
|
|
150
|
+
});
|
|
151
|
+
const { result } = await diff(gettingStarted, state);
|
|
152
|
+
const paths = driftPaths(result);
|
|
153
|
+
expect(paths).toEqual(expect.arrayContaining([expect.stringMatching(/^serviceOverview:changed:panels\[\d+\]\.panels\[\d+\]\.title$/)]));
|
|
154
|
+
const top = result.drifted.find((e) => e.name === "serviceOverview")!.changes.filter((c) => !c.path.startsWith("panels"));
|
|
155
|
+
expect(top.map((c) => `${c.path}=${String(c.live)}`).sort()).toEqual(["refresh=5m", "time.from=now-24h"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("a panel deleted in the UI is reported absent", async () => {
|
|
159
|
+
const state = edited(gettingStarted, "service-overview", (spec) => {
|
|
160
|
+
spec.panels = panels(spec).filter((x) => x.title !== "About");
|
|
161
|
+
});
|
|
162
|
+
const { result } = await diff(gettingStarted, state);
|
|
163
|
+
// Positional: the first panel is now the row that followed it, so its declared title moved.
|
|
164
|
+
expect(driftPaths(result)).toEqual(expect.arrayContaining(["serviceOverview:changed:panels[0].title"]));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("a panel added in the UI is reported as unclaimed, with its values", async () => {
|
|
168
|
+
const state = edited(gettingStarted, "service-overview", (spec) => {
|
|
169
|
+
panels(spec).push({ type: "stat", title: "Added in the UI", id: 99, gridPos: { h: 4, w: 6, x: 0, y: 100 }, targets: [{ refId: "A", expr: "up", datasource: { type: "prometheus", uid: "prometheus" } }] });
|
|
170
|
+
});
|
|
171
|
+
const { result } = await diff(gettingStarted, state);
|
|
172
|
+
expect(unclaimedPaths(result)).toEqual(expect.arrayContaining([expect.stringMatching(/^serviceOverview:panels\[\d+\]\.title$/)]));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("a dashboard moved to another folder", async () => {
|
|
176
|
+
const state = provisioned(gettingStarted, "v1");
|
|
177
|
+
state.folders!["f-other"] = "Other";
|
|
178
|
+
state.dashboards["service-overview"].folderUid = "f-other";
|
|
179
|
+
const { result } = await diff(gettingStarted, state);
|
|
180
|
+
expect(driftPaths(result)).toEqual(["serviceOverview:changed:folder"]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("the same edit reads the same through the legacy API (Grafana 11)", async () => {
|
|
184
|
+
const state = provisioned(gettingStarted, "legacy");
|
|
185
|
+
(panels(state.dashboards["service-overview"].spec).find((x) => x.title === "Requests per second by operation")!.targets as Json[])[0].expr = "up";
|
|
186
|
+
const { result } = await diff(gettingStarted, state);
|
|
187
|
+
expect(driftPaths(result)).toEqual([expect.stringMatching(/^serviceOverview:changed:panels\[\d+\]\.panels\[\d+\]\.targets\[0\]\.expr$/)]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("datasources", () => {
|
|
192
|
+
it("a changed URL is drift; secrets compare by key and never by value", async () => {
|
|
193
|
+
const state = provisioned(gettingStarted, "v1");
|
|
194
|
+
state.datasources!.prometheus = liveDatasource({ name: "Prometheus", type: "prometheus", uid: "prometheus", url: "http://elsewhere:9090", isDefault: true, secure: ["basicAuthPassword"] });
|
|
195
|
+
const { live, result } = await diff(gettingStarted, state);
|
|
196
|
+
const props = normalizeDeepObservation(live).resources.prometheus.properties;
|
|
197
|
+
expect(props.secureJsonData).toEqual({ basicAuthPassword: "[REDACTED]" });
|
|
198
|
+
const changes = result.drifted.find((e) => e.name === "prometheus")?.changes ?? [];
|
|
199
|
+
expect(changes.map((c) => `${c.kind}:${c.path}`)).toContain("changed:url");
|
|
200
|
+
expect(JSON.stringify(result)).not.toMatch(/elsewhere.*password|hunter2/);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("a dashboard Grafana stores as v2 (#2947)", () => {
|
|
205
|
+
it("is read at v2 and diffed through its classic form: the tabs a UI save made are drift", async () => {
|
|
206
|
+
const state = provisioned(gettingStarted, "v1");
|
|
207
|
+
// Saved from the new editor as the tabs dashboard of the import fixtures, under this dashboard's uid.
|
|
208
|
+
const v2 = (JSON.parse(readFileSync(join(import.meta.dirname, "..", "test", "fixtures", "exports", "grafana-13.2.2", "tabs.v2-resource.json"), "utf-8")) as Json).spec as Json;
|
|
209
|
+
state.dashboards["service-overview"] = { ...state.dashboards["service-overview"], storedVersion: "v2", v2 };
|
|
210
|
+
const { live, result } = await diff(gettingStarted, state);
|
|
211
|
+
const { resources, unobserved } = normalizeDeepObservation(live);
|
|
212
|
+
expect(unobserved.serviceOverview).toBeUndefined();
|
|
213
|
+
expect(resources.serviceOverview.properties.title).toBe("Checkout (tabs)");
|
|
214
|
+
const paths = driftPaths(result).filter((p) => p.startsWith("serviceOverview:"));
|
|
215
|
+
expect(paths).toContain("serviceOverview:changed:title");
|
|
216
|
+
// The tabs are rows in the live tree.
|
|
217
|
+
const rows = (resources.serviceOverview.properties.panels as Json[]).map((p) => p.title);
|
|
218
|
+
expect(rows).toEqual(expect.arrayContaining(["Overview", "Details"]));
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("what cannot be read is not observed, never a clean tree", () => {
|
|
223
|
+
it("a dashboard stored as v2 that the server does not serve at v2 is unsupported-kind, not its lossy classic read", async () => {
|
|
224
|
+
const state = provisioned(gettingStarted, "v1");
|
|
225
|
+
state.dashboards["service-overview"].storedVersion = "v2beta1";
|
|
226
|
+
const { live } = await diff(gettingStarted, state);
|
|
227
|
+
const { resources, unobserved } = normalizeDeepObservation(live);
|
|
228
|
+
expect(resources.serviceOverview).toBeUndefined();
|
|
229
|
+
expect(unobserved.serviceOverview).toMatchObject({ reason: "unsupported-kind", detail: expect.stringMatching(/stored as a v2beta1 dashboard .* lossy down-conversion/) });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("a refused token is no-credentials for every entity", async () => {
|
|
233
|
+
const { live } = await diff(gettingStarted, { ...provisioned(gettingStarted, "v1"), status: 401 });
|
|
234
|
+
const { resources, unobserved } = normalizeDeepObservation(live);
|
|
235
|
+
expect(resources).toEqual({});
|
|
236
|
+
expect(Object.keys(unobserved).sort()).toEqual([...gettingStarted.entities.keys()].sort());
|
|
237
|
+
expect(new Set(Object.values(unobserved).map((u) => u.reason))).toEqual(new Set(["no-credentials"]));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("no binding is no-binding for every entity", async () => {
|
|
241
|
+
const live = await observeResourcesDeepGrafana({ environment: "staging", entityNames: [], entities: gettingStarted.entities, config: CONFIG, env: {} });
|
|
242
|
+
const { unobserved } = normalizeDeepObservation(live);
|
|
243
|
+
expect(new Set(Object.values(unobserved).map((u) => u.reason))).toEqual(new Set(["no-binding"]));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("a server error is read-failed for that entity alone", async () => {
|
|
247
|
+
const state = provisioned(gettingStarted, "v1");
|
|
248
|
+
const base = fakeGrafana(state);
|
|
249
|
+
const http: typeof base = async (method, path, body) => (path.includes("/datasources/uid/tempo") ? { status: 500, json: { message: "boom" } } : base(method, path, body));
|
|
250
|
+
const live = await observeResourcesDeepGrafana({ environment: "prod", entityNames: [], entities: gettingStarted.entities, config: CONFIG, http });
|
|
251
|
+
const { resources, unobserved } = normalizeDeepObservation(live);
|
|
252
|
+
expect(unobserved.tempo).toMatchObject({ reason: "read-failed", detail: expect.stringContaining("500") });
|
|
253
|
+
expect(resources.serviceOverview).toBeDefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("owned: true withholds a dashboard another provider manages, and every datasource", async () => {
|
|
257
|
+
const state = provisioned(gettingStarted, "v1");
|
|
258
|
+
state.dashboards["service-overview"].annotations = { "grafana.app/managedBy": "classic-file-provisioning", "grafana.app/managerId": "someone-else" };
|
|
259
|
+
const live = await observeResourcesDeepGrafana({ environment: "prod", entityNames: [], entities: gettingStarted.entities, config: CONFIG, http: fakeGrafana(state), owned: true });
|
|
260
|
+
const { resources, unobserved } = normalizeDeepObservation(live);
|
|
261
|
+
expect(resources).toEqual({});
|
|
262
|
+
expect(unobserved.serviceOverview.reason).toBe("filtered");
|
|
263
|
+
expect(unobserved.prometheus.reason).toBe("filtered");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grafana deep observation (#2946): the property tree of each declared
|
|
3
|
+
* dashboard and datasource as Grafana holds it, so a dashboard edited in the
|
|
4
|
+
* UI shows up in `chant lifecycle diff --live` path by path.
|
|
5
|
+
*
|
|
6
|
+
* The read is the thin read's (./describe-resources.ts): same binding, same
|
|
7
|
+
* discovered API, same uid. What differs is what is done with the answer.
|
|
8
|
+
*
|
|
9
|
+
* - A dashboard's stored JSON goes through the importer
|
|
10
|
+
* (./import/live-export.ts `dashboardTree`), which puts it into the
|
|
11
|
+
* vocabulary its declaration is written in: `graphTooltip:
|
|
12
|
+
* "sharedCrosshair"` rather than `1`, rows holding their panels, queries
|
|
13
|
+
* without the refIds the build numbered. Core then diffs that against the
|
|
14
|
+
* declaration's props, normalized on both sides by
|
|
15
|
+
* ./deep-observe-hooks.ts.
|
|
16
|
+
* - A dashboard Grafana stores as v2 is read at v2 and converted to the
|
|
17
|
+
* classic JSON the importer makes of it (#2947), so the tree is
|
|
18
|
+
* diffed against the declaration: a tab added in the UI is a row that
|
|
19
|
+
* the declaration does not have.
|
|
20
|
+
* - The folder is not in the JSON. It is read from the resource's folder
|
|
21
|
+
* and written as its title, the form `Dashboard.folder` takes; when the
|
|
22
|
+
* declaration's folder name is one the build had to rewrite to make a
|
|
23
|
+
* directory (`Ops/Team` becomes `Ops-Team`), the declared spelling is
|
|
24
|
+
* kept, since the two name the same folder.
|
|
25
|
+
* - A datasource's API payload is put into `DatasourceProps` vocabulary,
|
|
26
|
+
* secrets as key names only.
|
|
27
|
+
*
|
|
28
|
+
* Tri-state, one level down: a dashboard that is not there is left out (the
|
|
29
|
+
* thin read reports it missing); one that cannot be read (a failed
|
|
30
|
+
* request, withheld by `owned`, a v2-stored dashboard the server cannot
|
|
31
|
+
* serve at v2) is NOT-OBSERVED with its reason; a
|
|
32
|
+
* provider has no API and is `unsupported-kind`. Nothing unreadable comes
|
|
33
|
+
* back as a clean tree. A declared panel, row, query or variable has no tree
|
|
34
|
+
* of its own (./members.ts): its properties are in its dashboard's.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { DeepObservationResult, DeepResourceObservation, UnobservedEntity } from "@intentius/chant/lexicon";
|
|
38
|
+
import { deepObservation, normalizeDeepProperties } from "@intentius/chant/deep-observation";
|
|
39
|
+
import { boundedConcurrently, unobservedAll } from "@intentius/chant/observation";
|
|
40
|
+
import { bindGrafana, classifyGrafanaFailure } from "./api/bind";
|
|
41
|
+
import type { GrafanaClient } from "./api/client";
|
|
42
|
+
import { classicDashboardOf, dashboardApi, folderTitleOf, readDashboard } from "./api/dashboards";
|
|
43
|
+
import { readDatasource } from "./api/datasources";
|
|
44
|
+
import { DASHBOARD_PROVIDER_TYPE, DASHBOARD_TYPE } from "./dashboard";
|
|
45
|
+
import { DATASOURCE_TYPE, EXTERNAL_DATASOURCE_TYPE } from "./datasource";
|
|
46
|
+
import { grafanaDeepNormalizationHooks } from "./deep-observe-hooks";
|
|
47
|
+
import { ORPHAN_PART, PROVIDER_NOT_OBSERVABLE, declaredUid, type GrafanaObserveOptions } from "./describe-resources";
|
|
48
|
+
import { dashboardMembers, isDashboardPart } from "./members";
|
|
49
|
+
import { dashboardTree, datasourceProps } from "./import/live-export";
|
|
50
|
+
import { chantProviderNames, dashboardOwnership, ownershipGap } from "./ownership";
|
|
51
|
+
|
|
52
|
+
export { grafanaDeepNormalizationHooks };
|
|
53
|
+
|
|
54
|
+
type Json = Record<string, unknown>;
|
|
55
|
+
|
|
56
|
+
/** The directory name the build writes a folder to (build.ts `folderDir`), which is the title Grafana gives it. */
|
|
57
|
+
function folderDirName(folder: string): string {
|
|
58
|
+
return folder.replace(/[/\\]+/g, "-").replace(/^\.+/, "").trim() || "General";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function dashboardProperties(client: GrafanaClient, uid: string, declared: Json, providers: ReadonlySet<string>, owned: boolean | undefined): Promise<DeepResourceObservation | UnobservedEntity | undefined> {
|
|
62
|
+
const read = await readDashboard(client, uid);
|
|
63
|
+
if ("absent" in read) return undefined;
|
|
64
|
+
const live = read.present;
|
|
65
|
+
const classic = classicDashboardOf(live);
|
|
66
|
+
if ("unsupported" in classic) {
|
|
67
|
+
return { type: DASHBOARD_TYPE, reason: "unsupported-kind", detail: `dashboard "${uid}" is not readable: ${classic.unsupported}`, queried: live.address };
|
|
68
|
+
}
|
|
69
|
+
if (owned) {
|
|
70
|
+
const { ownership } = dashboardOwnership(live, providers);
|
|
71
|
+
if (ownership !== "owned") {
|
|
72
|
+
return { type: DASHBOARD_TYPE, reason: "filtered", detail: `dashboard "${uid}" is ${ownership === "unknown" ? "of unknown ownership" : "not chant's"}: ${ownershipGap(live)}`, queried: live.address };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const { tree } = dashboardTree(classic.json);
|
|
76
|
+
const folder = await folderTitleOf(client, live);
|
|
77
|
+
if (folder !== undefined) {
|
|
78
|
+
const declaredFolder = typeof declared.folder === "string" ? declared.folder : undefined;
|
|
79
|
+
tree.folder = declaredFolder !== undefined && folderDirName(declaredFolder) === folder ? declaredFolder : folder;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
type: DASHBOARD_TYPE,
|
|
83
|
+
physicalId: uid,
|
|
84
|
+
properties: normalizeDeepProperties(tree, { entityType: DASHBOARD_TYPE, side: "live", hooks: grafanaDeepNormalizationHooks }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function datasourceProperties(client: GrafanaClient, uid: string, entityType: string, owned: boolean | undefined): Promise<DeepResourceObservation | UnobservedEntity | undefined> {
|
|
89
|
+
const read = await readDatasource(client, uid);
|
|
90
|
+
if ("absent" in read) return undefined;
|
|
91
|
+
if (owned) {
|
|
92
|
+
return { type: entityType, reason: "filtered", detail: `datasource "${uid}" exists but its ownership cannot be read: Grafana's datasource API returns no labels or annotations`, queried: read.address };
|
|
93
|
+
}
|
|
94
|
+
const props = datasourceProps(read.present);
|
|
95
|
+
// An ExternalDatasource declares only what panels refer to it by.
|
|
96
|
+
const tree = entityType === EXTERNAL_DATASOURCE_TYPE ? { type: props.type, uid: props.uid, name: props.name } : props;
|
|
97
|
+
return {
|
|
98
|
+
type: entityType,
|
|
99
|
+
physicalId: uid,
|
|
100
|
+
properties: normalizeDeepProperties(tree, { entityType, side: "live", hooks: grafanaDeepNormalizationHooks }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isUnobserved(v: DeepResourceObservation | UnobservedEntity): v is UnobservedEntity {
|
|
105
|
+
return "reason" in v;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Read the live property tree of every declared grafana entity. */
|
|
109
|
+
export async function observeResourcesDeepGrafana(options: GrafanaObserveOptions): Promise<DeepObservationResult> {
|
|
110
|
+
let client: GrafanaClient;
|
|
111
|
+
try {
|
|
112
|
+
client = await bindGrafana({ ...options });
|
|
113
|
+
await dashboardApi(client);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const { reason, detail } = classifyGrafanaFailure(err);
|
|
116
|
+
return deepObservation({}, unobservedAll(options.entities.keys(), reason, detail, options.entities));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const providers = chantProviderNames(options.entities.values());
|
|
120
|
+
const members = dashboardMembers(options.entities);
|
|
121
|
+
const resources: Record<string, DeepResourceObservation> = {};
|
|
122
|
+
const unobserved: Record<string, UnobservedEntity> = {};
|
|
123
|
+
|
|
124
|
+
await boundedConcurrently([...options.entities], async ([name, { entityType, props }]) => {
|
|
125
|
+
if (entityType === DASHBOARD_PROVIDER_TYPE) {
|
|
126
|
+
unobserved[name] = { type: entityType, reason: "unsupported-kind", detail: PROVIDER_NOT_OBSERVABLE };
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// A panel, row, query or variable is compared as part of its
|
|
130
|
+
// dashboard's tree, so it has no tree of its own: in neither map, which
|
|
131
|
+
// the deep diff reads as nothing to compare (helm does the same for its
|
|
132
|
+
// chart-authoring entities). One on no dashboard cannot be observed.
|
|
133
|
+
if (isDashboardPart(entityType)) {
|
|
134
|
+
if (!members.has(name)) unobserved[name] = { type: entityType, reason: "unsupported-kind", detail: ORPHAN_PART };
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const isDashboard = entityType === DASHBOARD_TYPE;
|
|
138
|
+
const isDatasource = entityType === DATASOURCE_TYPE || entityType === EXTERNAL_DATASOURCE_TYPE;
|
|
139
|
+
if (!isDashboard && !isDatasource) {
|
|
140
|
+
unobserved[name] = { type: entityType, reason: "unsupported-kind", detail: `no grafana deep reader for ${entityType}` };
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const uid = declaredUid(name, entityType, props);
|
|
144
|
+
if (!uid) {
|
|
145
|
+
unobserved[name] = { type: entityType, reason: "read-failed", detail: `"${name}" does not resolve to a uid` };
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const result = isDashboard
|
|
150
|
+
? await dashboardProperties(client, uid, props, providers, options.owned)
|
|
151
|
+
: await datasourceProperties(client, uid, entityType, options.owned);
|
|
152
|
+
// Not there: the thin read reports it missing; restating it as a property hole would say it twice.
|
|
153
|
+
if (result === undefined) return;
|
|
154
|
+
if (isUnobserved(result)) unobserved[name] = result;
|
|
155
|
+
else resources[name] = result;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
// A partial read never comes back as a clean tree.
|
|
158
|
+
const { reason, detail } = classifyGrafanaFailure(err);
|
|
159
|
+
unobserved[name] = { type: entityType, reason, detail };
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return deepObservation(resources, unobserved);
|
|
164
|
+
}
|