@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,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "A dashboard edited in Grafana's UI shows as drift in `chant lifecycle
|
|
3
|
+
* diff --live`" (#2946, #2960), against Grafana itself, 12.4.11 and 13.2.2.
|
|
4
|
+
*
|
|
5
|
+
* Per image: a small project (a datasource, a dashboard with a variable and
|
|
6
|
+
* a row of panels, a provider that allows UI saves) is built, its
|
|
7
|
+
* provisioning files and dashboards are mounted into a fresh container, and
|
|
8
|
+
* this checkout's `chant lifecycle diff <env> --live --json` is run in the
|
|
9
|
+
* project:
|
|
10
|
+
*
|
|
11
|
+
* 1. Untouched: every declared entity present, the dashboard `owned` (its
|
|
12
|
+
* provider is chant's), no drift and nothing unclaimed.
|
|
13
|
+
* 2. Then the dashboard is saved through `POST /api/dashboards/db`, the call
|
|
14
|
+
* the dashboard editor's Save makes, with one query and one panel title
|
|
15
|
+
* changed. The same command reports exactly those two paths as drift.
|
|
16
|
+
* 3. Live export of the environment generates TypeScript that carries the
|
|
17
|
+
* edit, and the generated project builds.
|
|
18
|
+
* 4. On 13.x only (#2947): the dashboard is recreated through the v2 API,
|
|
19
|
+
* so Grafana stores it as v2 and a classic read of it is a lossy
|
|
20
|
+
* down-conversion. (A v2 PUT over a dashboard stored as v0 keeps it at
|
|
21
|
+
* v0, converting v2-only layout away on write, so the test deletes it and
|
|
22
|
+
* creates it at v2.) The diff reads it at v2 instead. With a panel renamed
|
|
23
|
+
* the drift is exactly the classic-edit paths, so the v2 read adds no
|
|
24
|
+
* noise; with its row moved into a tab, the tab is a row the declaration
|
|
25
|
+
* does not have. Export names the tab.
|
|
26
|
+
*
|
|
27
|
+
* Containers come from the shared helpers in ../test/e2e/containers.ts
|
|
28
|
+
* (unique names, random host ports, removed in afterAll even on failure;
|
|
29
|
+
* `CHANT_GRAFANA_IMAGES` overrides the image pair). Skipped, with the reason
|
|
30
|
+
* in the test name, when Docker is not running. Run through the slot lock:
|
|
31
|
+
* `~/checkouts/intentius/chant-worktrees/.docker-slot.sh <label> -- npx vitest run lexicons/grafana/src/observe.e2e.test.ts`
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { execSync, spawn } from "node:child_process";
|
|
35
|
+
import { mkdirSync, symlinkSync, writeFileSync } from "node:fs";
|
|
36
|
+
import { dirname, join, resolve } from "node:path";
|
|
37
|
+
import { pathToFileURL } from "node:url";
|
|
38
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
39
|
+
import { build } from "@intentius/chant/build";
|
|
40
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
41
|
+
import { grafanaSerializer } from "./serializer";
|
|
42
|
+
import { exportResources } from "./export-resources";
|
|
43
|
+
import { GrafanaGenerator } from "./import/generator";
|
|
44
|
+
import { DockerScope, GRAFANA_IMAGES, dockerAvailable, waitFor, type GrafanaContainer } from "../test/e2e/containers";
|
|
45
|
+
|
|
46
|
+
const hasDocker = dockerAvailable();
|
|
47
|
+
const skipReason = hasDocker ? "" : " (skipped: Docker is not running)";
|
|
48
|
+
const repoRoot = resolve(import.meta.dirname, "..", "..", "..");
|
|
49
|
+
|
|
50
|
+
const PROJECT: Record<string, string> = {
|
|
51
|
+
"src/datasources.ts": `import { Datasource } from "@intentius/chant-lexicon-grafana";
|
|
52
|
+
|
|
53
|
+
export const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090", isDefault: true });
|
|
54
|
+
`,
|
|
55
|
+
"src/panels.ts": `import { PromQuery, Row, StatPanel, TimeSeriesPanel, CustomVariable } from "@intentius/chant-lexicon-grafana";
|
|
56
|
+
import { prometheus } from "./datasources";
|
|
57
|
+
|
|
58
|
+
export const job = new CustomVariable({ name: "job", values: ["api", "worker"] });
|
|
59
|
+
export const rate = new PromQuery({ expr: 'sum(rate(http_requests_total{job="$job"}[5m]))', datasource: prometheus });
|
|
60
|
+
export const errors = new PromQuery({ expr: 'sum(rate(http_requests_total{job="$job", code=~"5.."}[5m]))', datasource: prometheus });
|
|
61
|
+
const rateUnit = { defaults: { unit: "reqps" } };
|
|
62
|
+
export const ratePanel = new TimeSeriesPanel({ title: "Request rate", targets: [rate], fieldConfig: rateUnit });
|
|
63
|
+
export const errorPanel = new StatPanel({ title: "Errors", targets: [errors] });
|
|
64
|
+
export const traffic = new Row({ title: "Traffic", panels: [ratePanel, errorPanel] });
|
|
65
|
+
`,
|
|
66
|
+
"src/dashboard.ts": `import { Dashboard, DashboardProvider } from "@intentius/chant-lexicon-grafana";
|
|
67
|
+
import { job, traffic } from "./panels";
|
|
68
|
+
|
|
69
|
+
const lastHour = { from: "now-1h", to: "now" };
|
|
70
|
+
export const apiOverview = new Dashboard({ title: "API overview", tags: ["api"], time: lastHour, variables: [job], panels: [traffic] });
|
|
71
|
+
export const provider = new DashboardProvider({ name: "chant", allowUiUpdates: true, updateIntervalSeconds: 3600 });
|
|
72
|
+
`,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** This checkout's chant, run in `cwd`. */
|
|
76
|
+
function chant(cwd: string, env: Record<string, string>, ...args: string[]): Promise<{ status: number | null; stdout: string; stderr: string }> {
|
|
77
|
+
return new Promise((done) => {
|
|
78
|
+
const child = spawn(
|
|
79
|
+
process.execPath,
|
|
80
|
+
["--import", pathToFileURL(join(repoRoot, "node_modules/tsx/dist/loader.mjs")).href, join(repoRoot, "packages/core/src/cli/main.ts"), ...args],
|
|
81
|
+
{ cwd, env: { ...process.env, NO_COLOR: "1", ...env } },
|
|
82
|
+
);
|
|
83
|
+
let stdout = "";
|
|
84
|
+
let stderr = "";
|
|
85
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
86
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
87
|
+
const timer = setTimeout(() => child.kill(), 240_000);
|
|
88
|
+
child.on("close", (status) => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
done({ status, stdout, stderr });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface LiveDiffJson {
|
|
96
|
+
lexicons: {
|
|
97
|
+
grafana: {
|
|
98
|
+
resources: { missing: string[]; unobserved: Array<{ name: string; reason: string }> };
|
|
99
|
+
observed: Record<string, { ownership?: string; status: string }>;
|
|
100
|
+
deep: {
|
|
101
|
+
drifted: Array<{ name: string; changes: Array<{ path: string; kind: string; declared?: unknown; live?: unknown }> }>;
|
|
102
|
+
unclaimed: Array<{ name: string; fields: Array<{ path: string }> }>;
|
|
103
|
+
unchanged: string[];
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe.skipIf(!hasDocker).each(GRAFANA_IMAGES)(`a dashboard edited in Grafana shows as drift in chant lifecycle diff --live: %s${skipReason}`, (image) => {
|
|
110
|
+
const scope = new DockerScope("grafana-observe");
|
|
111
|
+
let dir = "";
|
|
112
|
+
let project = "";
|
|
113
|
+
let grafana: GrafanaContainer;
|
|
114
|
+
const env = { E2E_GRAFANA_USER: "admin", E2E_GRAFANA_PASSWORD: "admin", GRAFANA_URL: "", GRAFANA_TOKEN: "" };
|
|
115
|
+
const profile = () => ({ url: grafana.base, basicAuth: { user: { env: "E2E_GRAFANA_USER" }, password: { env: "E2E_GRAFANA_PASSWORD" } } });
|
|
116
|
+
|
|
117
|
+
const diff = async (): Promise<LiveDiffJson> => {
|
|
118
|
+
const r = await chant(project, env, "lifecycle", "diff", "e2e", "--live", "--json");
|
|
119
|
+
const line = r.stdout.trim().split("\n").filter((l) => l.startsWith("{")).pop();
|
|
120
|
+
if (!line) throw new Error(`no JSON from chant lifecycle diff (exit ${r.status}):\n${r.stdout}\n${r.stderr}`);
|
|
121
|
+
return JSON.parse(line) as LiveDiffJson;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
beforeAll(async () => {
|
|
125
|
+
dir = scope.tempDir();
|
|
126
|
+
project = join(dir, "project");
|
|
127
|
+
const mounted = join(dir, "mounted");
|
|
128
|
+
for (const [file, content] of Object.entries(PROJECT)) {
|
|
129
|
+
mkdirSync(dirname(join(project, file)), { recursive: true });
|
|
130
|
+
writeFileSync(join(project, file), content);
|
|
131
|
+
}
|
|
132
|
+
writeFileSync(join(project, "package.json"), JSON.stringify({ name: "grafana-observe-e2e", private: true, type: "module" }));
|
|
133
|
+
symlinkSync(join(repoRoot, "node_modules"), join(project, "node_modules"), "dir");
|
|
134
|
+
execSync("git init -q && git -c user.email=e2e@example.com -c user.name=e2e commit -q --allow-empty -m init", { cwd: project });
|
|
135
|
+
|
|
136
|
+
const result = await build(join(project, "src"), [grafanaSerializer]);
|
|
137
|
+
expect(result.errors).toEqual([]);
|
|
138
|
+
const out = result.outputs.get("grafana") as SerializerResult;
|
|
139
|
+
for (const [file, content] of Object.entries(out.files ?? {})) {
|
|
140
|
+
mkdirSync(dirname(join(mounted, file)), { recursive: true });
|
|
141
|
+
writeFileSync(join(mounted, file), content);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
grafana = await scope.grafana({ image, provisioningDir: join(mounted, "provisioning"), dashboardsDir: join(mounted, "dashboards") });
|
|
145
|
+
writeFileSync(join(project, "chant.config.ts"), `export default { lexicons: ["grafana"], grafana: { profiles: { e2e: ${JSON.stringify(profile())} } } };\n`);
|
|
146
|
+
|
|
147
|
+
await waitFor("the dashboard to be provisioned", async () => ((await grafana.api("/api/dashboards/uid/api-overview")).status === 200 ? true : undefined));
|
|
148
|
+
await waitFor("the datasource to be provisioned", async () => ((await grafana.api("/api/datasources/uid/prometheus")).status === 200 ? true : undefined));
|
|
149
|
+
}, 300_000);
|
|
150
|
+
|
|
151
|
+
afterAll(() => scope.cleanup());
|
|
152
|
+
|
|
153
|
+
it("an untouched dashboard: present, chant's, no drift, nothing unclaimed", { timeout: 300_000 }, async () => {
|
|
154
|
+
const report = (await diff()).lexicons.grafana;
|
|
155
|
+
expect(report.resources.missing).toEqual([]);
|
|
156
|
+
expect(report.observed.apiOverview).toMatchObject({ status: "PRESENT", ownership: "owned" });
|
|
157
|
+
expect(report.observed.prometheus).toMatchObject({ status: "PRESENT" });
|
|
158
|
+
// Only the provider is not observed: Grafana serves no API for it.
|
|
159
|
+
expect(report.resources.unobserved.map((u) => `${u.name}:${u.reason}`)).toEqual(["provider:unsupported-kind"]);
|
|
160
|
+
expect(report.deep.drifted).toEqual([]);
|
|
161
|
+
expect(report.deep.unclaimed).toEqual([]);
|
|
162
|
+
expect(report.deep.unchanged).toEqual(expect.arrayContaining(["apiOverview", "prometheus"]));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("the same dashboard saved from the editor with a changed query and title: exactly those paths drift", { timeout: 300_000 }, async () => {
|
|
166
|
+
const { body } = await grafana.api("/api/dashboards/uid/api-overview");
|
|
167
|
+
const dashboard = body.dashboard as { panels: Array<{ title: string; targets?: Array<{ expr?: string }> }> };
|
|
168
|
+
const rate = dashboard.panels.find((p) => p.title === "Request rate")!;
|
|
169
|
+
rate.targets![0].expr = "sum(rate(http_requests_total[1m]))";
|
|
170
|
+
dashboard.panels.find((p) => p.title === "Errors")!.title = "5xx";
|
|
171
|
+
const saved = await grafana.api("/api/dashboards/db", { method: "POST", body: JSON.stringify({ dashboard, overwrite: true, message: "edited in the UI" }) });
|
|
172
|
+
expect(saved.status).toBe(200);
|
|
173
|
+
|
|
174
|
+
const report = (await diff()).lexicons.grafana;
|
|
175
|
+
const changes = report.deep.drifted.find((d) => d.name === "apiOverview")?.changes ?? [];
|
|
176
|
+
expect(changes.map((c) => ({ path: c.path, kind: c.kind, live: c.live })).sort((a, b) => a.path.localeCompare(b.path))).toEqual([
|
|
177
|
+
{ path: "panels[0].panels[0].targets[0].expr", kind: "changed", live: "sum(rate(http_requests_total[1m]))" },
|
|
178
|
+
{ path: "panels[0].panels[1].title", kind: "changed", live: "5xx" },
|
|
179
|
+
]);
|
|
180
|
+
expect(report.deep.drifted.map((d) => d.name)).toEqual(["apiOverview"]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("live export generates TypeScript that carries the edit and builds", { timeout: 240_000 }, async () => {
|
|
184
|
+
const ir = await exportResources({
|
|
185
|
+
environment: "e2e",
|
|
186
|
+
config: { grafana: { profiles: { e2e: profile() } } },
|
|
187
|
+
env,
|
|
188
|
+
});
|
|
189
|
+
const files = new GrafanaGenerator().generate(ir);
|
|
190
|
+
const source = files.map((f) => f.content).join("\n");
|
|
191
|
+
expect(source).toContain("sum(rate(http_requests_total[1m]))");
|
|
192
|
+
expect(source).toContain('"5xx"');
|
|
193
|
+
|
|
194
|
+
// The generated project builds: the exported Datasource and the dashboard's reference to it do not collide.
|
|
195
|
+
const exported = join(dir, "exported");
|
|
196
|
+
for (const f of files) {
|
|
197
|
+
mkdirSync(dirname(join(exported, "src", f.path)), { recursive: true });
|
|
198
|
+
writeFileSync(join(exported, "src", f.path), f.content);
|
|
199
|
+
}
|
|
200
|
+
writeFileSync(join(exported, "package.json"), JSON.stringify({ name: "grafana-export-e2e", private: true, type: "module" }));
|
|
201
|
+
writeFileSync(join(exported, "chant.config.ts"), 'export default { lexicons: ["grafana"] };\n');
|
|
202
|
+
symlinkSync(join(repoRoot, "node_modules"), join(exported, "node_modules"), "dir");
|
|
203
|
+
const built = await chant(exported, {}, "build", "src", "--lexicon", "grafana", "-o", join(exported, "dist", "index.json"));
|
|
204
|
+
expect(built.status, `${built.stdout}\n${built.stderr}`).toBe(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it.runIf(/:13\./.test(image))("stored as v2: read at v2, a rename is drift at the classic path, and a tab is a row the declaration lacks", { timeout: 300_000 }, async () => {
|
|
208
|
+
const path = "/apis/dashboard.grafana.app/v2/namespaces/default/dashboards/api-overview";
|
|
209
|
+
type V2Spec = { elements: Record<string, { spec: { title: string } }>; layout: unknown };
|
|
210
|
+
/** Delete the dashboard and create it again at v2 with `edit` applied, so Grafana stores it as v2. */
|
|
211
|
+
const recreateAtV2 = async (edit: (spec: V2Spec) => void) => {
|
|
212
|
+
const { status, body: resource } = await grafana.api(path);
|
|
213
|
+
expect(status).toBe(200);
|
|
214
|
+
edit(resource.spec as V2Spec);
|
|
215
|
+
const deleted = await grafana.api(path, { method: "DELETE" });
|
|
216
|
+
expect(deleted.status, JSON.stringify(deleted.body)).toBe(200);
|
|
217
|
+
const { name, labels, annotations } = resource.metadata;
|
|
218
|
+
const created = await grafana.api("/apis/dashboard.grafana.app/v2/namespaces/default/dashboards", {
|
|
219
|
+
method: "POST",
|
|
220
|
+
body: JSON.stringify({ apiVersion: resource.apiVersion, kind: "Dashboard", metadata: { name, labels, annotations }, spec: resource.spec }),
|
|
221
|
+
});
|
|
222
|
+
expect(created.status, JSON.stringify(created.body)).toBeLessThan(300);
|
|
223
|
+
// Grafana stores it as v2 now: its v1 read says so, and is the lossy copy chant must not read.
|
|
224
|
+
const v1 = await grafana.api("/apis/dashboard.grafana.app/v1/namespaces/default/dashboards/api-overview");
|
|
225
|
+
expect(v1.body.status?.conversion?.storedVersion).toBe("v2");
|
|
226
|
+
};
|
|
227
|
+
const driftOf = async () => {
|
|
228
|
+
const report = (await diff()).lexicons.grafana;
|
|
229
|
+
expect(report.resources.unobserved.map((u) => `${u.name}:${u.reason}`)).toEqual(["provider:unsupported-kind"]);
|
|
230
|
+
// Observed, not unsupported-kind. Created through the API it is no longer the file provider's: Grafana does not
|
|
231
|
+
// keep the provisioning manager annotations a client sends, so the v2 resource's own metadata makes it foreign.
|
|
232
|
+
expect(report.observed.apiOverview).toMatchObject({ status: "PRESENT", ownership: "foreign" });
|
|
233
|
+
return (report.deep.drifted.find((d) => d.name === "apiOverview")?.changes ?? []).map((c) => ({ path: c.path, kind: c.kind, live: c.live }));
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Only a rename: the v2 read adds no noise, so the drift is the earlier edit plus the new title, at the paths a classic read gives.
|
|
237
|
+
await recreateAtV2((spec) => {
|
|
238
|
+
Object.values(spec.elements).find((e) => e.spec.title === "5xx")!.spec.title = "Errors (v2)";
|
|
239
|
+
});
|
|
240
|
+
expect((await driftOf()).sort((a, b) => a.path.localeCompare(b.path))).toEqual([
|
|
241
|
+
{ path: "panels[0].panels[0].targets[0].expr", kind: "changed", live: "sum(rate(http_requests_total[1m]))" },
|
|
242
|
+
{ path: "panels[0].panels[1].title", kind: "changed", live: "Errors (v2)" },
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
// The row moved into a tab: the tab is an expanded row ahead of it, which the declaration does not have.
|
|
246
|
+
await recreateAtV2((spec) => {
|
|
247
|
+
spec.layout = { kind: "TabsLayout", spec: { tabs: [{ kind: "TabsLayoutTab", spec: { title: "Everything", layout: spec.layout } }] } };
|
|
248
|
+
});
|
|
249
|
+
expect(await driftOf()).toContainEqual({ path: "panels[0].title", kind: "changed", live: "Everything" });
|
|
250
|
+
|
|
251
|
+
const ir = await exportResources({ environment: "e2e", config: { grafana: { profiles: { e2e: profile() } } }, env, selector: { name: "api-overview" } });
|
|
252
|
+
expect(ir.warnings).toContainEqual(expect.stringMatching(/^dashboard api-overview is stored as v2 and exported in the classic model: layout: the dashboard's tabs "Everything" become expanded rows/));
|
|
253
|
+
expect(new GrafanaGenerator().generate(ir).map((f) => f.content).join("\n")).toContain('"Errors (v2)"');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The API applier (#2948) against Grafana itself, 12.4.11 and 13.2.2.
|
|
3
|
+
*
|
|
4
|
+
* Per image, one project is built and applied with `grafanaApply`, the Op
|
|
5
|
+
* activity, through its build output (the index and the dashboard files):
|
|
6
|
+
*
|
|
7
|
+
* 1. Apply: two folders, a library panel and three dashboards are created,
|
|
8
|
+
* each dashboard and folder labelled with the project's marker.
|
|
9
|
+
* 2. Re-apply: nothing is created, and nothing is written.
|
|
10
|
+
* 3. A panel title is edited in the source and applied: that dashboard is
|
|
11
|
+
* updated, the rest unchanged.
|
|
12
|
+
* 4. A dashboard is removed from the source and applied with `prune`: it is
|
|
13
|
+
* deleted; a dashboard created directly in Grafana survives, and so does
|
|
14
|
+
* the removed dashboard's folder, since somebody saved a dashboard of
|
|
15
|
+
* their own into it.
|
|
16
|
+
* 5. Once that dashboard is gone, the next prune deletes the empty folder.
|
|
17
|
+
*
|
|
18
|
+
* The build does not write `__elements` yet (library panels are not
|
|
19
|
+
* declared in the lexicon), so the library panel is added to one built
|
|
20
|
+
* dashboard's file the way an exported dashboard carries it.
|
|
21
|
+
*
|
|
22
|
+
* Containers come from ../../../test/e2e/containers.ts (unique names,
|
|
23
|
+
* random host ports, removed in afterAll even on failure;
|
|
24
|
+
* `CHANT_GRAFANA_IMAGES` overrides the image pair). Skipped, with the reason
|
|
25
|
+
* in the test name, when Docker is not running. Run through the slot lock:
|
|
26
|
+
* `~/checkouts/intentius/chant-worktrees/.docker-slot.sh <label> -- npx vitest run lexicons/grafana/src/op/activities/grafana-apply.e2e.test.ts`
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { dirname, join, resolve } from "node:path";
|
|
31
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
32
|
+
import { build } from "@intentius/chant/build";
|
|
33
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
34
|
+
import { grafanaSerializer } from "../../serializer";
|
|
35
|
+
import { grafanaApply, type GrafanaApplyDeps, type GrafanaApplyOutcome } from "./grafana-apply";
|
|
36
|
+
import { DockerScope, GRAFANA_IMAGES, dockerAvailable, type GrafanaContainer } from "../../../test/e2e/containers";
|
|
37
|
+
|
|
38
|
+
const hasDocker = dockerAvailable();
|
|
39
|
+
const skipReason = hasDocker ? "" : " (skipped: Docker is not running)";
|
|
40
|
+
const repoRoot = resolve(import.meta.dirname, "..", "..", "..", "..", "..");
|
|
41
|
+
|
|
42
|
+
const MARKER_LABELS = { "app.kubernetes.io/managed-by": "chant", "chant.intentius.io/stack": "e2e-shop", "chant.intentius.io/env": "e2e" };
|
|
43
|
+
const LIBRARY_PANEL = { uid: "chant-e2e-burn", name: "Error budget burn", kind: 1, model: { type: "stat", title: "Error budget burn", description: "shared" } };
|
|
44
|
+
|
|
45
|
+
function source(opts: { requestsTitle: string; withErrors: boolean }): string {
|
|
46
|
+
return `import { Dashboard, StatPanel } from "@intentius/chant-lexicon-grafana";
|
|
47
|
+
|
|
48
|
+
export const requests = new StatPanel({ title: ${JSON.stringify(opts.requestsTitle)} });
|
|
49
|
+
export const latency = new StatPanel({ title: "Latency" });
|
|
50
|
+
export const fiveHundreds = new StatPanel({ title: "5xx" });
|
|
51
|
+
export const apiOverview = new Dashboard({ title: "API overview", folder: "Team A", tags: ["api"], panels: [requests, latency] });
|
|
52
|
+
${opts.withErrors ? `export const errors = new Dashboard({ title: "Errors", folder: "Team B", panels: [fiveHundreds] });\n` : ""}export const home = new Dashboard({ title: "Home" });
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const summary = (o: GrafanaApplyOutcome) => o.applied.map((a) => `${a.action} ${a.kind}/${a.name}`).sort();
|
|
57
|
+
|
|
58
|
+
describe.skipIf(!hasDocker).each(GRAFANA_IMAGES)(`grafanaApply applies, re-applies, updates and prunes against %s${skipReason}`, (image) => {
|
|
59
|
+
const scope = new DockerScope("grafana-apply");
|
|
60
|
+
let project = "";
|
|
61
|
+
let indexPath = "";
|
|
62
|
+
let grafana: GrafanaContainer;
|
|
63
|
+
let deps: GrafanaApplyDeps;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build the project as `opts` declares it, into `dist/`, with the library
|
|
67
|
+
* panel added to api-overview's file. Each build is a fresh project
|
|
68
|
+
* directory: the build imports the source modules, and a module path
|
|
69
|
+
* already imported in this process would be served from the cache.
|
|
70
|
+
*/
|
|
71
|
+
const buildProject = async (opts: { requestsTitle: string; withErrors: boolean }) => {
|
|
72
|
+
project = join(scope.tempDir(), "project");
|
|
73
|
+
mkdirSync(join(project, "src"), { recursive: true });
|
|
74
|
+
writeFileSync(join(project, "package.json"), JSON.stringify({ name: "grafana-apply-e2e", private: true, type: "module" }));
|
|
75
|
+
symlinkSync(join(repoRoot, "node_modules"), join(project, "node_modules"), "dir");
|
|
76
|
+
writeFileSync(join(project, "src", "dashboards.ts"), source(opts));
|
|
77
|
+
const result = await build(join(project, "src"), [grafanaSerializer]);
|
|
78
|
+
expect(result.errors).toEqual([]);
|
|
79
|
+
const out = result.outputs.get("grafana") as SerializerResult;
|
|
80
|
+
const dist = join(project, "dist");
|
|
81
|
+
rmSync(dist, { recursive: true, force: true });
|
|
82
|
+
for (const [file, content] of Object.entries(out.files ?? {})) {
|
|
83
|
+
mkdirSync(dirname(join(dist, file)), { recursive: true });
|
|
84
|
+
writeFileSync(join(dist, file), content);
|
|
85
|
+
}
|
|
86
|
+
indexPath = join(dist, "grafana.json");
|
|
87
|
+
writeFileSync(indexPath, out.primary);
|
|
88
|
+
const index = JSON.parse(out.primary) as { dashboards: Array<{ uid: string; file: string }> };
|
|
89
|
+
const file = join(dist, index.dashboards.find((d) => d.uid === "api-overview")!.file);
|
|
90
|
+
const json = JSON.parse(readFileSync(file, "utf8")) as { panels: unknown[] } & Record<string, unknown>;
|
|
91
|
+
json.__elements = { [LIBRARY_PANEL.uid]: LIBRARY_PANEL };
|
|
92
|
+
json.panels.push({ id: 99, gridPos: { x: 0, y: 20, w: 12, h: 6 }, libraryPanel: { uid: LIBRARY_PANEL.uid, name: LIBRARY_PANEL.name } });
|
|
93
|
+
writeFileSync(file, JSON.stringify(json, null, 2));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const apply = (prune = false) => grafanaApply({ indexPath, environment: "e2e", ...(prune ? { prune: true } : {}) }, undefined, deps);
|
|
97
|
+
const dashboard = (uid: string) => grafana.api(`/api/dashboards/uid/${uid}`);
|
|
98
|
+
const saveForeign = (uid: string, folderUid = "") =>
|
|
99
|
+
grafana.api("/api/dashboards/db", { method: "POST", body: JSON.stringify({ dashboard: { uid, title: `Saved in the UI (${uid})` }, folderUid, overwrite: true }) });
|
|
100
|
+
|
|
101
|
+
beforeAll(async () => {
|
|
102
|
+
grafana = await scope.grafana({ image });
|
|
103
|
+
deps = {
|
|
104
|
+
config: {
|
|
105
|
+
grafana: { profiles: { e2e: { url: grafana.base, basicAuth: { user: { env: "E2E_GRAFANA_USER" }, password: { env: "E2E_GRAFANA_PASSWORD" } } } } },
|
|
106
|
+
ownership: { stack: "e2e-shop", env: "e2e" },
|
|
107
|
+
},
|
|
108
|
+
env: { E2E_GRAFANA_USER: "admin", E2E_GRAFANA_PASSWORD: "admin" },
|
|
109
|
+
};
|
|
110
|
+
expect((await saveForeign("foreign-home")).status).toBe(200);
|
|
111
|
+
}, 300_000);
|
|
112
|
+
|
|
113
|
+
afterAll(() => scope.cleanup());
|
|
114
|
+
|
|
115
|
+
it("apply: creates the folders, the library panel and the dashboards, labelled with the marker", { timeout: 240_000 }, async () => {
|
|
116
|
+
await buildProject({ requestsTitle: "Requests", withErrors: true });
|
|
117
|
+
const out = await apply();
|
|
118
|
+
expect(out.api).toMatch(/^apis\/v1(beta1)?$/);
|
|
119
|
+
expect(out.notAttempted).toEqual([]);
|
|
120
|
+
expect(summary(out)).toEqual([
|
|
121
|
+
"created Dashboard/api-overview",
|
|
122
|
+
"created Dashboard/errors",
|
|
123
|
+
"created Dashboard/home",
|
|
124
|
+
"created Folder/team-a",
|
|
125
|
+
"created Folder/team-b",
|
|
126
|
+
"created LibraryPanel/chant-e2e-burn",
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const path = out.applied.find((a) => a.name === "api-overview")!.address;
|
|
130
|
+
const live = (await grafana.api(path)).body as { metadata: { labels: Record<string, string>; annotations: Record<string, string> } };
|
|
131
|
+
expect(live.metadata.labels).toMatchObject(MARKER_LABELS);
|
|
132
|
+
expect(live.metadata.annotations["grafana.app/folder"]).toBe("team-a");
|
|
133
|
+
const folder = (await grafana.api(out.applied.find((a) => a.name === "team-a")!.address)).body as { metadata: { labels: Record<string, string> }; spec: { title: string } };
|
|
134
|
+
expect(folder.metadata.labels).toMatchObject(MARKER_LABELS);
|
|
135
|
+
expect(folder.spec.title).toBe("Team A");
|
|
136
|
+
const connections = (await grafana.api(`/api/library-elements/${LIBRARY_PANEL.uid}/connections`)).body as { result: Array<{ connectionUid: string }> };
|
|
137
|
+
expect(connections.result.map((c) => c.connectionUid)).toEqual(["api-overview"]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("re-apply: nothing is created and nothing changes", { timeout: 240_000 }, async () => {
|
|
141
|
+
const out = await apply();
|
|
142
|
+
expect(out.applied.filter((a) => a.action === "created")).toEqual([]);
|
|
143
|
+
expect(out.applied.filter((a) => a.action !== "unchanged")).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("an edited declaration: that dashboard is updated, the rest unchanged", { timeout: 240_000 }, async () => {
|
|
147
|
+
await buildProject({ requestsTitle: "Requests per second", withErrors: true });
|
|
148
|
+
const out = await apply();
|
|
149
|
+
expect(out.applied.filter((a) => a.action !== "unchanged").map((a) => `${a.action} ${a.kind}/${a.name}`)).toEqual(["updated Dashboard/api-overview"]);
|
|
150
|
+
const body = (await dashboard("api-overview")).body as { dashboard: { panels: Array<{ title?: string }> } };
|
|
151
|
+
expect(body.dashboard.panels.map((p) => p.title)).toContain("Requests per second");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("a removed declaration is pruned; what Grafana users made survives, and so does the folder they saved into", { timeout: 240_000 }, async () => {
|
|
155
|
+
expect((await saveForeign("foreign-in-b", "team-b")).status).toBe(200);
|
|
156
|
+
await buildProject({ requestsTitle: "Requests per second", withErrors: false });
|
|
157
|
+
const out = await apply(true);
|
|
158
|
+
expect(out.pruned.map((p) => `${p.kind}/${p.name}:${p.deleted}`)).toEqual(["Dashboard/errors:true"]);
|
|
159
|
+
expect(out.notAttempted).toEqual([expect.objectContaining({ kind: "Folder", name: "team-b", reason: "not-prunable" })]);
|
|
160
|
+
expect((await dashboard("errors")).status).toBe(404);
|
|
161
|
+
expect((await dashboard("foreign-home")).status).toBe(200);
|
|
162
|
+
expect((await dashboard("foreign-in-b")).status).toBe(200);
|
|
163
|
+
expect((await dashboard("api-overview")).status).toBe(200);
|
|
164
|
+
expect((await dashboard("home")).status).toBe(200);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("once the folder is empty, the next prune deletes it", { timeout: 240_000 }, async () => {
|
|
168
|
+
expect((await grafana.api("/api/dashboards/uid/foreign-in-b", { method: "DELETE" })).status).toBe(200);
|
|
169
|
+
const out = await apply(true);
|
|
170
|
+
expect(out.pruned.map((p) => `${p.kind}/${p.name}:${p.deleted}`)).toEqual(["Folder/team-b:true"]);
|
|
171
|
+
expect(out.notAttempted).toEqual([]);
|
|
172
|
+
expect((await grafana.api("/api/folders/team-b")).status).toBe(404);
|
|
173
|
+
expect((await grafana.api("/api/folders/team-a")).status).toBe(200);
|
|
174
|
+
});
|
|
175
|
+
});
|