@intentius/chant-lexicon-grafana 0.99.0 → 0.100.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 +12 -6
- package/dist/annotations.d.ts +61 -0
- package/dist/annotations.d.ts.map +1 -0
- package/dist/api/apply.d.ts +8 -4
- package/dist/api/apply.d.ts.map +1 -1
- package/dist/api/fake-grafana.d.ts +2 -0
- package/dist/api/fake-grafana.d.ts.map +1 -1
- package/dist/api/folders.d.ts +53 -17
- package/dist/api/folders.d.ts.map +1 -1
- package/dist/build.d.ts +26 -5
- 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/generate.d.ts +7 -1
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/composites/shared.d.ts +4 -3
- package/dist/composites/shared.d.ts.map +1 -1
- package/dist/dashboard.d.ts +42 -14
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/datasource-refs.d.ts +9 -5
- package/dist/datasource-refs.d.ts.map +1 -1
- package/dist/datasource-settings.d.ts +834 -0
- package/dist/datasource-settings.d.ts.map +1 -0
- package/dist/datasource.d.ts +53 -11
- package/dist/datasource.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts +1 -1
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/deep-observe.d.ts +9 -4
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +1 -0
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/detect.d.ts +4 -2
- package/dist/detect.d.ts.map +1 -1
- package/dist/folder.d.ts +68 -0
- package/dist/folder.d.ts.map +1 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/mappings.d.ts +12 -6
- package/dist/import/mappings.d.ts.map +1 -1
- package/dist/import/model.d.ts +7 -0
- package/dist/import/model.d.ts.map +1 -1
- package/dist/import/normalize.d.ts.map +1 -1
- package/dist/import/parser.d.ts +16 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/import/testdata/fixtures.d.ts +6 -0
- package/dist/import/testdata/fixtures.d.ts.map +1 -1
- package/dist/import/v2.d.ts +4 -3
- package/dist/import/v2.d.ts.map +1 -1
- package/dist/index.d.ts +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/init-templates.d.ts +27 -0
- package/dist/init-templates.d.ts.map +1 -0
- package/dist/integrity.json +11 -9
- package/dist/lint/audit-catalog.d.ts +1 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/graf101.d.ts +1 -1
- package/dist/lint/post-synth/graf104.d.ts +2 -2
- package/dist/lint/post-synth/graf106.d.ts +2 -2
- package/dist/lint/post-synth/graf110.d.ts +8 -0
- package/dist/lint/post-synth/graf110.d.ts.map +1 -0
- 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/manifest.json +1 -1
- package/dist/meta.json +70 -0
- package/dist/okf/index.md +18 -3
- package/dist/okf/rules/GRAF101.md +2 -2
- package/dist/okf/rules/GRAF104.md +2 -2
- package/dist/okf/rules/GRAF106.md +2 -2
- package/dist/okf/rules/GRAF110.md +11 -0
- package/dist/okf/types/AdhocVariable.md +9 -0
- package/dist/okf/types/AzureMonitorQuery.md +9 -0
- package/dist/okf/types/BigQueryQuery.md +9 -0
- package/dist/okf/types/CloudMonitoringQuery.md +9 -0
- package/dist/okf/types/CloudWatchQuery.md +9 -0
- package/dist/okf/types/Dashboard.md +1 -1
- package/dist/okf/types/Datasource.md +1 -1
- package/dist/okf/types/DatasourceProvisioning.md +9 -0
- package/dist/okf/types/ElasticsearchQuery.md +9 -0
- package/dist/okf/types/ExternalDatasource.md +1 -1
- package/dist/okf/types/Folder.md +9 -0
- package/dist/okf/types/GroupByVariable.md +9 -0
- package/dist/okf/types/MSSQLQuery.md +9 -0
- package/dist/okf/types/MySQLQuery.md +9 -0
- package/dist/okf/types/PostgresQuery.md +9 -0
- package/dist/okf/types/PyroscopeQuery.md +9 -0
- package/dist/okf/types/SwitchVariable.md +9 -0
- package/dist/op/activities/grafana-apply.d.ts +11 -0
- package/dist/op/activities/grafana-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/ownership.d.ts +11 -0
- package/dist/ownership.d.ts.map +1 -1
- package/dist/pin.d.ts +1 -1
- package/dist/pin.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/promql-check.d.ts.map +1 -1
- package/dist/query-models.d.ts +95 -0
- package/dist/query-models.d.ts.map +1 -0
- package/dist/query.d.ts +107 -10
- package/dist/query.d.ts.map +1 -1
- package/dist/rules/graf101.ts +2 -2
- package/dist/rules/graf104.ts +3 -3
- package/dist/rules/graf106.ts +3 -3
- package/dist/rules/graf110.ts +17 -0
- package/dist/rules/grafana-helpers.ts +13 -3
- package/dist/schema/azuremonitor.gen.d.ts +354 -0
- package/dist/schema/azuremonitor.gen.d.ts.map +1 -0
- package/dist/schema/bigquery.gen.d.ts +93 -0
- package/dist/schema/bigquery.gen.d.ts.map +1 -0
- package/dist/schema/cloudwatch.gen.d.ts +394 -0
- package/dist/schema/cloudwatch.gen.d.ts.map +1 -0
- package/dist/schema/elasticsearch.gen.d.ts +444 -0
- package/dist/schema/elasticsearch.gen.d.ts.map +1 -0
- package/dist/schema/googlecloudmonitoring.gen.d.ts +312 -0
- package/dist/schema/googlecloudmonitoring.gen.d.ts.map +1 -0
- package/dist/schema/grafanapyroscope.gen.d.ts +60 -0
- package/dist/schema/grafanapyroscope.gen.d.ts.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/table.gen.d.ts +13 -0
- package/dist/schema/table.gen.d.ts.map +1 -1
- package/dist/schema-validate.d.ts +9 -0
- package/dist/schema-validate.d.ts.map +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-grafana-operations.md +108 -0
- package/dist/skills/chant-grafana.md +5 -4
- package/dist/spec/schemas.d.ts +9 -7
- package/dist/spec/schemas.d.ts.map +1 -1
- package/dist/spec/schemas.gen.d.ts +3 -0
- package/dist/spec/schemas.gen.d.ts.map +1 -0
- package/dist/validate-alerting.d.ts.map +1 -1
- package/dist/validate-output.d.ts +15 -2
- package/dist/validate-output.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validation.d.ts +14 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/variables.d.ts +102 -5
- package/dist/variables.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/annotations.ts +84 -0
- package/src/api/apply.test.ts +19 -1
- package/src/api/apply.ts +18 -8
- package/src/api/fake-grafana.ts +6 -2
- package/src/api/folders.ts +106 -32
- package/src/build.test.ts +142 -6
- package/src/build.ts +126 -24
- package/src/catalog.ts +20 -2
- package/src/codegen/docs.ts +13 -6
- package/src/codegen/generate.ts +27 -1
- package/src/composites/shared.ts +3 -2
- package/src/dashboard.ts +59 -14
- package/src/datasource-refs.ts +30 -6
- package/src/datasource-settings.test.ts +106 -0
- package/src/datasource-settings.ts +941 -0
- package/src/datasource.ts +71 -11
- package/src/deep-observe-hooks.ts +16 -1
- package/src/deep-observe.test.ts +47 -1
- package/src/deep-observe.ts +36 -13
- package/src/describe-resources.test.ts +16 -0
- package/src/describe-resources.ts +36 -1
- package/src/detect.ts +3 -3
- package/src/folder.ts +118 -0
- package/src/generated/lexicon-grafana.json +70 -0
- package/src/import/generated-types.e2e.test.ts +15 -3
- package/src/import/generator.ts +2 -1
- package/src/import/mappings.ts +98 -26
- package/src/import/model.ts +7 -0
- package/src/import/normalize.ts +32 -3
- package/src/import/parser.test.ts +97 -21
- package/src/import/parser.ts +169 -18
- package/src/import/roundtrip.test.ts +104 -9
- package/src/import/testdata/fixtures.ts +8 -0
- package/src/import/v2.ts +4 -3
- package/src/index.ts +122 -13
- package/src/init-templates.test.ts +74 -0
- package/src/init-templates.ts +349 -0
- package/src/lint/audit-catalog.ts +9 -1
- package/src/lint/post-synth/graf101.ts +2 -2
- package/src/lint/post-synth/graf104.ts +3 -3
- package/src/lint/post-synth/graf106.ts +3 -3
- package/src/lint/post-synth/graf110.ts +17 -0
- package/src/lint/post-synth/grafana-helpers.ts +13 -3
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/post-synth.test.ts +99 -2
- package/src/load-cost.test.ts +35 -0
- package/src/op/activities/grafana-apply.e2e.test.ts +14 -5
- package/src/op/activities/grafana-apply.test.ts +16 -2
- package/src/op/activities/grafana-apply.ts +29 -3
- package/src/op/activities/index.ts +1 -1
- package/src/ownership.ts +19 -0
- package/src/panels.test.ts +3 -1
- package/src/pin.ts +12 -0
- package/src/plugin.test.ts +9 -2
- package/src/plugin.ts +9 -0
- package/src/promql-check.ts +5 -1
- package/src/query-models.ts +94 -0
- package/src/query.test.ts +166 -0
- package/src/query.ts +177 -12
- package/src/schema/azuremonitor.gen.ts +381 -0
- package/src/schema/bigquery.gen.ts +110 -0
- package/src/schema/cloudwatch.gen.ts +423 -0
- package/src/schema/elasticsearch.gen.ts +508 -0
- package/src/schema/googlecloudmonitoring.gen.ts +330 -0
- package/src/schema/grafanapyroscope.gen.ts +66 -0
- package/src/schema/index.ts +6 -0
- package/src/schema/table.gen.ts +14 -0
- package/src/schema-validate.ts +58 -7
- package/src/skill-defs.ts +17 -0
- package/src/skills/chant-grafana-operations.md +108 -0
- package/src/skills/chant-grafana.md +5 -4
- package/src/spec/overlay/table.overlay.json +19 -0
- package/src/spec/schemas/azuremonitor.jsonschema.json +679 -0
- package/src/spec/schemas/bigquery.jsonschema.json +290 -0
- package/src/spec/schemas/cloudwatch.jsonschema.json +721 -0
- package/src/spec/schemas/elasticsearch.jsonschema.json +1695 -0
- package/src/spec/schemas/googlecloudmonitoring.jsonschema.json +488 -0
- package/src/spec/schemas/grafanapyroscope.jsonschema.json +93 -0
- package/src/spec/schemas.gen.ts +40 -0
- package/src/spec/schemas.ts +9 -7
- package/src/validate-alerting.ts +4 -2
- package/src/validate-output.ts +96 -4
- package/src/validate.ts +14 -0
- package/src/validation.ts +27 -0
- package/src/variables.ts +113 -6
package/src/import/parser.ts
CHANGED
|
@@ -47,6 +47,7 @@ import * as jsYaml from "js-yaml";
|
|
|
47
47
|
import type { TemplateIR, TemplateParser, ResourceIR } from "@intentius/chant/import/parser";
|
|
48
48
|
import { DASHBOARD_SCHEMA_VERSION } from "../schema/dashboard.gen";
|
|
49
49
|
import { BUILTIN_DATASOURCE_UIDS } from "../datasource";
|
|
50
|
+
import { JSONDATA_LINKS } from "../datasource-settings";
|
|
50
51
|
import {
|
|
51
52
|
looksLikeDashboard,
|
|
52
53
|
looksLikeAlertingProvisioning,
|
|
@@ -149,9 +150,21 @@ const PACKAGE_CLASS_NAMES = new Set([
|
|
|
149
150
|
"DatasourceVariable",
|
|
150
151
|
"ConstantVariable",
|
|
151
152
|
"TextboxVariable",
|
|
153
|
+
"AdhocVariable",
|
|
154
|
+
"GroupByVariable",
|
|
155
|
+
"SwitchVariable",
|
|
152
156
|
"PromQuery",
|
|
153
157
|
"TempoQuery",
|
|
154
158
|
"LokiQuery",
|
|
159
|
+
"ElasticsearchQuery",
|
|
160
|
+
"CloudWatchQuery",
|
|
161
|
+
"AzureMonitorQuery",
|
|
162
|
+
"CloudMonitoringQuery",
|
|
163
|
+
"BigQueryQuery",
|
|
164
|
+
"PyroscopeQuery",
|
|
165
|
+
"PostgresQuery",
|
|
166
|
+
"MySQLQuery",
|
|
167
|
+
"MSSQLQuery",
|
|
155
168
|
"TimeSeriesPanel",
|
|
156
169
|
"StatPanel",
|
|
157
170
|
"GaugePanel",
|
|
@@ -187,6 +200,8 @@ const PACKAGE_CLASS_NAMES = new Set([
|
|
|
187
200
|
"MuteTiming",
|
|
188
201
|
"NotificationTemplate",
|
|
189
202
|
"ExternalDatasource",
|
|
203
|
+
"DatasourceProvisioning",
|
|
204
|
+
"Folder",
|
|
190
205
|
]);
|
|
191
206
|
|
|
192
207
|
function words(text: string): string[] {
|
|
@@ -526,6 +541,52 @@ class DashboardConverter {
|
|
|
526
541
|
return ids;
|
|
527
542
|
}
|
|
528
543
|
|
|
544
|
+
// ── annotations ─────────────────────────────────────────────────
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* `annotations.list` as `Dashboard.annotations`: every entry but the
|
|
548
|
+
* built-in one Grafana adds anyway, with its datasource resolved like a
|
|
549
|
+
* panel's and every other key as it is. `enable` and `iconColor`, which
|
|
550
|
+
* the schema requires and the build fills in, are recorded when missing.
|
|
551
|
+
*/
|
|
552
|
+
annotations(): Json[] {
|
|
553
|
+
const container = this.d.annotations;
|
|
554
|
+
if (container === undefined || container === null) return [];
|
|
555
|
+
if (!isObject(container) || !Array.isArray(container.list)) {
|
|
556
|
+
this.report.drop(pointer("annotations"), "dashboard", "annotations", "(it is not an annotations.list)");
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
for (const key of Object.keys(container)) if (key !== "list") this.report.drop(pointer("annotations", key), "annotations", key, NO_PROP);
|
|
560
|
+
const out: Json[] = [];
|
|
561
|
+
container.list.forEach((a, i) => {
|
|
562
|
+
if (isBuiltinAnnotation(a)) return;
|
|
563
|
+
const path = (...rest: string[]) => pointer("annotations", "list", i, ...rest);
|
|
564
|
+
if (!isObject(a) || typeof a.name !== "string") {
|
|
565
|
+
this.report.drop(path(), "annotations", `entry ${i}`, "(it is not a named annotation query)");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const subject = `annotation "${a.name}"`;
|
|
569
|
+
const props: Json = {};
|
|
570
|
+
for (const [key, value] of Object.entries(a)) {
|
|
571
|
+
if (value === null || value === undefined) {
|
|
572
|
+
this.report.drop(path(key), subject, key);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (key !== "datasource") {
|
|
576
|
+
props[key] = value;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const ds = this.resolveDatasource(value, path(key), subject);
|
|
580
|
+
if (ds === "mixed") this.report.drop(path(key), subject, key, MIXED_ELSEWHERE);
|
|
581
|
+
else if (ds) props.datasource = ds.value;
|
|
582
|
+
}
|
|
583
|
+
if (typeof a.enable !== "boolean") this.report.replace(path("enable"), true, subject, "enable", "is missing, so it is written as true, which runs the query");
|
|
584
|
+
if (typeof a.iconColor !== "string") this.report.replace(path("iconColor"), "red", subject, "iconColor", "is missing, so it is written as red");
|
|
585
|
+
out.push(props);
|
|
586
|
+
});
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
|
|
529
590
|
// ── variables ───────────────────────────────────────────────────
|
|
530
591
|
|
|
531
592
|
variable(json: unknown, index: number): string | undefined {
|
|
@@ -539,7 +600,7 @@ class DashboardConverter {
|
|
|
539
600
|
const type = String(json.type);
|
|
540
601
|
const mapping = VARIABLE_MAPPINGS[type];
|
|
541
602
|
if (!mapping) {
|
|
542
|
-
this.report.drop(path, subject, "", `(${type}) is not carried: chant has no ${type} variable
|
|
603
|
+
this.report.drop(path, subject, "", `(${type}) is not carried: chant has no ${type} variable, so it is left out`);
|
|
543
604
|
return undefined;
|
|
544
605
|
}
|
|
545
606
|
let dropped = false;
|
|
@@ -883,6 +944,7 @@ class DashboardConverter {
|
|
|
883
944
|
const variableRefs = [...inputIds, ...templating.map((_, i) => varIds.get(i)).filter((x): x is string => x !== undefined)].map(declRef);
|
|
884
945
|
|
|
885
946
|
const panelRefs = this.items();
|
|
947
|
+
const annotations = this.annotations();
|
|
886
948
|
this.settleExternals();
|
|
887
949
|
if (this.legacyPanels > 0) {
|
|
888
950
|
this.report.warn(
|
|
@@ -920,18 +982,7 @@ class DashboardConverter {
|
|
|
920
982
|
if (variableRefs.length > 0) props.variables = variableRefs;
|
|
921
983
|
if (panelRefs.length > 0) props.panels = panelRefs;
|
|
922
984
|
|
|
923
|
-
|
|
924
|
-
const annotations = isObject(d.annotations) && Array.isArray(d.annotations.list) ? d.annotations.list : [];
|
|
925
|
-
const others = annotations.filter((a) => !isBuiltinAnnotation(a));
|
|
926
|
-
if (others.length > 0) {
|
|
927
|
-
const names = others.map((a) => (isObject(a) && typeof a.name === "string" ? `"${a.name}"` : "(unnamed)"));
|
|
928
|
-
this.report.drop(
|
|
929
|
-
pointer("annotations"),
|
|
930
|
-
"dashboard",
|
|
931
|
-
"",
|
|
932
|
-
`has ${others.length === 1 ? "the annotation" : "the annotations"} ${list(names)}, which ${others.length === 1 ? "is" : "are"} not carried: Dashboard has no annotations yet (#2953)`,
|
|
933
|
-
);
|
|
934
|
-
}
|
|
985
|
+
if (annotations.length > 0) props.annotations = annotations;
|
|
935
986
|
|
|
936
987
|
const handled = new Set([
|
|
937
988
|
...DASHBOARD_FIELDS,
|
|
@@ -1032,10 +1083,55 @@ const DATASOURCE_FIELDS = [
|
|
|
1032
1083
|
"version",
|
|
1033
1084
|
];
|
|
1034
1085
|
|
|
1035
|
-
/**
|
|
1086
|
+
/**
|
|
1087
|
+
* The `jsonData` paths of `ds` that hold another datasource's uid (see
|
|
1088
|
+
* `JSONDATA_LINKS`), with the uid found there and the plugin types the
|
|
1089
|
+
* field accepts.
|
|
1090
|
+
*/
|
|
1091
|
+
function jsonDataLinks(type: string, jsonData: Json): Array<{ path: Array<string | number>; uid: string; targets: readonly string[] }> {
|
|
1092
|
+
const out: Array<{ path: Array<string | number>; uid: string; targets: readonly string[] }> = [];
|
|
1093
|
+
const walk = (value: unknown, rest: readonly string[], at: Array<string | number>, targets: readonly string[]) => {
|
|
1094
|
+
if (rest.length === 0) {
|
|
1095
|
+
if (typeof value === "string" && value !== "") out.push({ path: at, uid: value, targets });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const [head, ...tail] = rest;
|
|
1099
|
+
if (head === "*") {
|
|
1100
|
+
if (Array.isArray(value)) value.forEach((v, i) => walk(v, tail, [...at, i], targets));
|
|
1101
|
+
} else if (isObject(value)) walk(value[head], tail, [...at, head], targets);
|
|
1102
|
+
};
|
|
1103
|
+
for (const link of JSONDATA_LINKS) if (!link.on || link.on.includes(type)) walk(jsonData, link.path, [], link.targets);
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** A copy of `value` with `replacement` at `path`. */
|
|
1108
|
+
function replaceAt(value: unknown, path: ReadonlyArray<string | number>, replacement: unknown): unknown {
|
|
1109
|
+
if (path.length === 0) return replacement;
|
|
1110
|
+
const [head, ...tail] = path;
|
|
1111
|
+
if (Array.isArray(value)) return value.map((v, i) => (i === head ? replaceAt(v, tail, replacement) : v));
|
|
1112
|
+
if (isObject(value)) return { ...value, [head]: replaceAt(value[head], tail, replacement) };
|
|
1113
|
+
return value;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Plan a datasource provisioning file: one `Datasource` per entry, and a
|
|
1118
|
+
* `DatasourceProvisioning` for the file's own settings when they differ
|
|
1119
|
+
* from what the build writes by default (`prune: true`, no deletes): a file
|
|
1120
|
+
* that does not prune gets `prune: false`, so the rebuilt file still does
|
|
1121
|
+
* not.
|
|
1122
|
+
*
|
|
1123
|
+
* A uid in one entry's `jsonData` that names another entry of the file (a
|
|
1124
|
+
* Tempo's `tracesToLogsV2.datasourceUid`, a Prometheus exemplar's
|
|
1125
|
+
* `datasourceUid`, a Loki derived field's) becomes a reference to that
|
|
1126
|
+
* entry's `Datasource`, so the link is type-checked, when the entry is of a
|
|
1127
|
+
* plugin type the field accepts. Declarations are ordered so each refers
|
|
1128
|
+
* only to ones before it; a link that would close a cycle (Tempo to Loki and
|
|
1129
|
+
* Loki back to Tempo) stays a uid string. The build writes provisioned
|
|
1130
|
+
* datasources sorted by name, so the order does not show in its output.
|
|
1131
|
+
*/
|
|
1036
1132
|
export function planDatasourceProvisioning(doc: Json): { plan: Plan; warnings: string[] } {
|
|
1037
1133
|
const report = new Report();
|
|
1038
|
-
const
|
|
1134
|
+
const entries: Array<{ id: string; name: string; type: string; props: Json; uid?: string }> = [];
|
|
1039
1135
|
const list = Array.isArray(doc.datasources) ? doc.datasources : [];
|
|
1040
1136
|
list.forEach((ds, i) => {
|
|
1041
1137
|
if (!isObject(ds) || typeof ds.name !== "string" || typeof ds.type !== "string") {
|
|
@@ -1047,12 +1143,67 @@ export function planDatasourceProvisioning(doc: Json): { plan: Plan; warnings: s
|
|
|
1047
1143
|
for (const key of Object.keys(ds)) {
|
|
1048
1144
|
if (!DATASOURCE_FIELDS.includes(key)) report.drop(pointer("datasources", i, key), `datasource "${ds.name}"`, key, NO_PROP);
|
|
1049
1145
|
}
|
|
1050
|
-
|
|
1146
|
+
entries.push({ id: `datasource:${i}`, name: ds.name, type: ds.type, props, ...(typeof ds.uid === "string" ? { uid: ds.uid } : {}) });
|
|
1051
1147
|
});
|
|
1148
|
+
const settings: Json = {};
|
|
1149
|
+
if (doc.prune !== true) settings.prune = false;
|
|
1150
|
+
if (doc.prune !== undefined && doc.prune !== null && typeof doc.prune !== "boolean") {
|
|
1151
|
+
report.drop(pointer("prune"), "the provisioning file", "prune", `(${JSON.stringify(doc.prune)} is not true or false, so the file is written not to prune)`);
|
|
1152
|
+
}
|
|
1153
|
+
const deletes: Json[] = [];
|
|
1154
|
+
(Array.isArray(doc.deleteDatasources) ? doc.deleteDatasources : []).forEach((d, i) => {
|
|
1155
|
+
if (!isObject(d) || typeof d.name !== "string") {
|
|
1156
|
+
report.drop(pointer("deleteDatasources", i), "deleteDatasources", `entry ${i}`, "(it names no datasource)");
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const orgId = typeof d.orgId === "number" ? d.orgId : undefined;
|
|
1160
|
+
for (const key of Object.keys(d)) if (key !== "name" && key !== "orgId") report.drop(pointer("deleteDatasources", i, key), `deleteDatasources "${d.name}"`, key, NO_PROP);
|
|
1161
|
+
deletes.push({ name: d.name, ...(orgId !== undefined ? { orgId } : {}) });
|
|
1162
|
+
});
|
|
1163
|
+
if (deletes.length > 0) settings.deleteDatasources = deletes;
|
|
1164
|
+
if (doc.deleteDatasources !== undefined && !Array.isArray(doc.deleteDatasources)) report.drop(pointer("deleteDatasources"), "the provisioning file", "deleteDatasources", "(it is not a list)");
|
|
1165
|
+
const settingsDeclarations: Declaration[] = [];
|
|
1166
|
+
if (Object.keys(settings).length > 0) {
|
|
1167
|
+
settingsDeclarations.push({ id: "datasource-provisioning", kind: "new", className: "DatasourceProvisioning", props: settings, name: "datasource provisioning", module: "datasources" });
|
|
1168
|
+
}
|
|
1052
1169
|
for (const key of Object.keys(doc)) {
|
|
1053
|
-
if (key === "apiVersion" || key === "datasources") continue;
|
|
1054
|
-
report.drop(pointer(key), "the provisioning file", key,
|
|
1170
|
+
if (key === "apiVersion" || key === "datasources" || key === "prune" || key === "deleteDatasources") continue;
|
|
1171
|
+
report.drop(pointer(key), "the provisioning file", key, NO_PROP);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Each entry's links to other entries of the file, by the uid they name.
|
|
1175
|
+
const byUid = new Map(entries.filter((e) => e.uid !== undefined).map((e) => [e.uid!, e]));
|
|
1176
|
+
const links = new Map(
|
|
1177
|
+
entries.map((e) => {
|
|
1178
|
+
const jsonData = e.props.jsonData;
|
|
1179
|
+
const found = isObject(jsonData) ? jsonDataLinks(e.type, jsonData) : [];
|
|
1180
|
+
return [e.id, found.flatMap((l) => {
|
|
1181
|
+
const target = byUid.get(l.uid);
|
|
1182
|
+
return target && target !== e && l.targets.includes(target.type) ? [{ path: l.path, target: target.id }] : [];
|
|
1183
|
+
})];
|
|
1184
|
+
}),
|
|
1185
|
+
);
|
|
1186
|
+
// File order, except that an entry waits for the entries it links to; a cycle is broken at the first entry left.
|
|
1187
|
+
const declared = new Set<string>();
|
|
1188
|
+
const declarations: Declaration[] = [];
|
|
1189
|
+
while (declared.size < entries.length) {
|
|
1190
|
+
const remaining = entries.filter((e) => !declared.has(e.id));
|
|
1191
|
+
const next = remaining.find((e) => links.get(e.id)!.every((l) => declared.has(l.target))) ?? remaining[0];
|
|
1192
|
+
let jsonData = next.props.jsonData;
|
|
1193
|
+
for (const l of links.get(next.id)!) if (declared.has(l.target)) jsonData = replaceAt(jsonData, l.path, declRef(l.target));
|
|
1194
|
+
const props = jsonData === undefined ? next.props : { ...next.props, jsonData };
|
|
1195
|
+
declarations.push({
|
|
1196
|
+
id: next.id,
|
|
1197
|
+
kind: "new",
|
|
1198
|
+
className: "Datasource",
|
|
1199
|
+
typeArguments: [JSON.stringify(next.type)],
|
|
1200
|
+
props,
|
|
1201
|
+
name: next.name,
|
|
1202
|
+
module: "datasources",
|
|
1203
|
+
});
|
|
1204
|
+
declared.add(next.id);
|
|
1055
1205
|
}
|
|
1206
|
+
declarations.push(...settingsDeclarations);
|
|
1056
1207
|
return {
|
|
1057
1208
|
plan: {
|
|
1058
1209
|
directory: "",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* the check that nothing was dropped silently.
|
|
15
15
|
*
|
|
16
16
|
* The generated source must lint clean (COR001, COR004 and COR009 among
|
|
17
|
-
* the rules), and the rebuilt dashboards must pass GRAF101-
|
|
17
|
+
* the rules), and the rebuilt dashboards must pass GRAF101-GRAF110 without
|
|
18
18
|
* errors. generated-types.e2e.test.ts type-checks the generated source.
|
|
19
19
|
*/
|
|
20
20
|
|
|
@@ -34,7 +34,19 @@ import { GrafanaParser, type DashboardResourceMetadata } from "./parser";
|
|
|
34
34
|
import { GrafanaGenerator } from "./generator";
|
|
35
35
|
import { applyEdits } from "./edits";
|
|
36
36
|
import { normalizeDashboard } from "./normalize";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
COMMUNITY,
|
|
39
|
+
KUBE_PROMETHEUS,
|
|
40
|
+
LOSSY_V1_EXPORT,
|
|
41
|
+
PROVISIONING,
|
|
42
|
+
UI_EXPORTS,
|
|
43
|
+
V2_EXPORTS,
|
|
44
|
+
exampleOutputs,
|
|
45
|
+
projectDir,
|
|
46
|
+
read,
|
|
47
|
+
removeDir,
|
|
48
|
+
writeFiles,
|
|
49
|
+
} from "./testdata/fixtures";
|
|
38
50
|
|
|
39
51
|
type Json = Record<string, unknown>;
|
|
40
52
|
|
|
@@ -54,7 +66,7 @@ interface Imported {
|
|
|
54
66
|
lint: { errorCount: number; warningCount: number; output: string };
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
/** GRAF101-
|
|
69
|
+
/** GRAF101-GRAF110 over a build's grafana entities, as the post-synth checks run them: declared and external datasources included. */
|
|
58
70
|
function checks(entities: Map<string, Declarable>): GrafanaIssue[] {
|
|
59
71
|
const built = buildGrafana(new Map([...entities].filter(([, e]) => e.lexicon === "grafana")));
|
|
60
72
|
return validateGrafanaOutput({
|
|
@@ -173,12 +185,32 @@ describe("dashboard JSON -> TypeScript -> dashboard JSON", () => {
|
|
|
173
185
|
// GRAF101 saying it cannot check a dashboard that names its datasources only through variables.
|
|
174
186
|
expect(out.issues.filter((i) => !(i.code === "GRAF101" && i.severity === "warning" && i.message.includes("cannot check")))).toEqual([]);
|
|
175
187
|
expect(out.paths.every((p) => p.startsWith("chant-fx-"))).toBe(true);
|
|
176
|
-
// The ad hoc filter and the annotation are
|
|
188
|
+
// The ad hoc filter and the deploy annotation are carried (#2952, #2953).
|
|
177
189
|
if (file.includes("checkout")) {
|
|
178
|
-
expect(out.
|
|
179
|
-
expect(out.warnings
|
|
190
|
+
expect(out.source).toContain("const filters = new AdhocVariable({");
|
|
191
|
+
expect(out.warnings.join("\n")).not.toContain("Deploys");
|
|
192
|
+
expect(out.source).toContain('name: "Deploys"');
|
|
193
|
+
expect((out.rebuilt!.annotations as { list: Json[] }).list.map((a) => a.name)).toEqual(["Deploys"]);
|
|
180
194
|
expect(out.source).toContain("repeat: env,");
|
|
181
195
|
expect(out.source).toContain("collapsed: true,");
|
|
196
|
+
} else if (file.includes("queries")) {
|
|
197
|
+
// Every datasource with a query class is imported through it, typed; none needs a defineQuery.
|
|
198
|
+
for (const cls of [
|
|
199
|
+
"ElasticsearchQuery",
|
|
200
|
+
"CloudWatchQuery",
|
|
201
|
+
"AzureMonitorQuery",
|
|
202
|
+
"CloudMonitoringQuery",
|
|
203
|
+
"BigQueryQuery",
|
|
204
|
+
"PyroscopeQuery",
|
|
205
|
+
"PostgresQuery",
|
|
206
|
+
"MySQLQuery",
|
|
207
|
+
"MSSQLQuery",
|
|
208
|
+
"PromQuery",
|
|
209
|
+
"LokiQuery",
|
|
210
|
+
]) {
|
|
211
|
+
expect(out.source).toContain(`new ${cls}(`);
|
|
212
|
+
}
|
|
213
|
+
expect(out.source).not.toContain("defineQuery");
|
|
182
214
|
} else if (file.includes("slo")) {
|
|
183
215
|
expect(out.warnings).toContainEqual(expect.stringMatching(/is a library panel \("Burn rate", uid chant-fx-burn\)/));
|
|
184
216
|
}
|
|
@@ -190,7 +222,7 @@ describe("dashboard JSON -> TypeScript -> dashboard JSON", () => {
|
|
|
190
222
|
expect(out.warnings).toContain("dashboard: __elements is not carried (the library panels exported with it; library panels are not carried yet)");
|
|
191
223
|
}
|
|
192
224
|
if (file.includes(".external.")) {
|
|
193
|
-
expect(out.warnings).toContainEqual(expect.stringMatching(/^__inputs: DS_PROMETHEUS \(prometheus\)/));
|
|
225
|
+
expect(out.warnings).toContainEqual(expect.stringMatching(/^__inputs: (.*, )?DS_PROMETHEUS \(prometheus\)/));
|
|
194
226
|
expect(out.source).toContain("const dsPrometheus = new DatasourceVariable({");
|
|
195
227
|
}
|
|
196
228
|
});
|
|
@@ -205,6 +237,10 @@ describe("dashboard JSON -> TypeScript -> dashboard JSON", () => {
|
|
|
205
237
|
// Tabs become rows, and the auto grid's panels keep the positions Grafana gives them.
|
|
206
238
|
expect(out.warnings).toContainEqual(expect.stringMatching(/tabs "Overview" and "Details" become expanded rows/));
|
|
207
239
|
expect((out.rebuilt!.panels as Json[]).filter((p) => p.type === "row").map((p) => p.title)).toEqual(["Overview", "Details", "Latency ($env)", "Logs"]);
|
|
240
|
+
// Its switch and group by variables come through the classic variable mappings.
|
|
241
|
+
expect(out.source).toContain('const detailed = new SwitchVariable({ name: "detailed", label: "Detailed" });');
|
|
242
|
+
expect(out.source).toContain("const groupBy = new GroupByVariable({");
|
|
243
|
+
expect(out.warnings.filter((w) => /^variable "(detailed|groupBy)"/.test(w))).toEqual([]);
|
|
208
244
|
}
|
|
209
245
|
});
|
|
210
246
|
}
|
|
@@ -241,6 +277,23 @@ describe("dashboard JSON -> TypeScript -> dashboard JSON", () => {
|
|
|
241
277
|
expect(out.text).not.toContain("__inputs");
|
|
242
278
|
});
|
|
243
279
|
|
|
280
|
+
test("ad hoc, group by and switch variables, and object-form queries, import with no warnings", async () => {
|
|
281
|
+
const out = await expectRoundTrip(read("exports/grafana-13.2.2/drilldown.json"));
|
|
282
|
+
expect(out.warnings).toEqual([]);
|
|
283
|
+
for (const cls of ["AdhocVariable", "GroupByVariable", "SwitchVariable"]) expect(out.source).toContain(`new ${cls}({`);
|
|
284
|
+
expect(out.source).toContain("qryType: 4,");
|
|
285
|
+
expect(out.source).toContain('enabledValue: "0.99",');
|
|
286
|
+
// The rebuilt JSON keeps the object query, so Grafana's variable editor opens it in the form it was written in.
|
|
287
|
+
const namespace = (out.rebuilt!.templating as { list: Json[] }).list.find((v) => v.name === "namespace")!;
|
|
288
|
+
expect(namespace.query).toEqual({ qryType: 1, query: "label_values(kube_namespace_created, namespace)", refId: "PrometheusVariableQueryEditor-VariableQuery" });
|
|
289
|
+
// Repeated over a multi-value variable: no GRAF110.
|
|
290
|
+
expect(out.issues.filter((i) => i.code === "GRAF110")).toEqual([]);
|
|
291
|
+
for (const file of ["exports/grafana-12.4.11/drilldown.json", "exports/grafana-12.4.11/drilldown.external.json", "exports/grafana-13.2.2/drilldown.external.json"]) {
|
|
292
|
+
const warnings = (await expectRoundTrip(read(file))).warnings;
|
|
293
|
+
expect(warnings.filter((w) => !w.startsWith("__inputs:") && !w.startsWith("dashboard: __requires")), file).toEqual([]);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
244
297
|
for (const file of COMMUNITY) {
|
|
245
298
|
test(`grafana.com dashboard ${file}`, async () => {
|
|
246
299
|
const out = await expectRoundTrip(read(file));
|
|
@@ -343,6 +396,46 @@ describe("provisioning files", () => {
|
|
|
343
396
|
expect(out.lint.errorCount + out.lint.warningCount, out.lint.output).toBe(0);
|
|
344
397
|
});
|
|
345
398
|
|
|
399
|
+
for (const file of PROVISIONING) {
|
|
400
|
+
test(`${file}: typed Datasources, links as references, the same datasources back`, async () => {
|
|
401
|
+
const yaml = read(file);
|
|
402
|
+
const out = await importAndBuild(yaml);
|
|
403
|
+
expect(out.warnings).toEqual([]);
|
|
404
|
+
expect(out.buildErrors).toEqual([]);
|
|
405
|
+
// What the build adds: its defaults for access and editable, and a uid made from the name.
|
|
406
|
+
type Entry = Record<string, unknown> & { name: string };
|
|
407
|
+
const withDefaults = (d: Entry) => ({ access: "proxy", editable: false, ...d });
|
|
408
|
+
const sorted = (list: Entry[]) => [...list].sort((a, b) => a.name.localeCompare(b.name));
|
|
409
|
+
const source = sorted((load(yaml) as { datasources: Entry[] }).datasources.map(withDefaults));
|
|
410
|
+
expect(sorted((load(out.files[DATASOURCES_FILE]) as { datasources: Entry[] }).datasources)).toEqual(source);
|
|
411
|
+
// Settings consts are typed for their plugin, and a uid naming an earlier datasource of the file is a reference.
|
|
412
|
+
expect(out.source).toContain('PropsOf<typeof Datasource<"tempo">>["jsonData"]');
|
|
413
|
+
expect(out.source).toMatch(/serviceMap: \{ datasourceUid: (mimir|prometheus) \}/);
|
|
414
|
+
expect(out.lint.errorCount + out.lint.warningCount, out.lint.output).toBe(0);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
test("a link that would close a cycle stays a uid, and one to a plugin type the field does not take is not linked", () => {
|
|
419
|
+
const ir = new GrafanaParser().parse(
|
|
420
|
+
[
|
|
421
|
+
"apiVersion: 1",
|
|
422
|
+
"datasources:",
|
|
423
|
+
" - { name: Tempo, type: tempo, uid: tempo, jsonData: { tracesToLogsV2: { datasourceUid: loki }, serviceMap: { datasourceUid: splunk } } }",
|
|
424
|
+
" - { name: Loki, type: loki, uid: loki, jsonData: { derivedFields: [{ name: t, matcherRegex: x, datasourceUid: tempo }] } }",
|
|
425
|
+
" - { name: Splunk, type: grafana-splunk-datasource, uid: splunk }",
|
|
426
|
+
].join("\n"),
|
|
427
|
+
);
|
|
428
|
+
const source = new GrafanaGenerator()
|
|
429
|
+
.generate(ir)
|
|
430
|
+
.map((f) => f.content)
|
|
431
|
+
.join("\n");
|
|
432
|
+
// Neither Tempo nor Loki can come first with both links; Tempo, first in the file, keeps its link as a uid.
|
|
433
|
+
expect(source).toContain('tracesToLogsV2: { datasourceUid: "loki" }');
|
|
434
|
+
expect(source).toContain("datasourceUid: tempo");
|
|
435
|
+
// The service map takes a Prometheus, so a Splunk uid stays a string.
|
|
436
|
+
expect(source).toContain('serviceMap: { datasourceUid: "splunk" }');
|
|
437
|
+
});
|
|
438
|
+
|
|
346
439
|
test("a dashboard provisioning file becomes DashboardProviders", async () => {
|
|
347
440
|
const yaml = [
|
|
348
441
|
"apiVersion: 1",
|
|
@@ -382,13 +475,15 @@ describe("chant import dashboard.json", () => {
|
|
|
382
475
|
expect(result.error).toBeUndefined();
|
|
383
476
|
expect(result.success).toBe(true);
|
|
384
477
|
expect(result.lexicon).toBe("grafana");
|
|
478
|
+
// Nine variables (the two __inputs datasources among them): more than COR009's eight per module.
|
|
385
479
|
expect(result.generatedFiles).toEqual([
|
|
386
|
-
"chant-fx-checkout/variables.ts",
|
|
480
|
+
"chant-fx-checkout/variables-1.ts",
|
|
481
|
+
"chant-fx-checkout/variables-2.ts",
|
|
387
482
|
"chant-fx-checkout/panels.ts",
|
|
388
483
|
"chant-fx-checkout/row-details-for-job.ts",
|
|
389
484
|
"chant-fx-checkout/dashboard.ts",
|
|
390
485
|
]);
|
|
391
|
-
expect(result.warnings
|
|
486
|
+
expect(result.warnings.join("\n")).not.toContain("is not carried: chant has no");
|
|
392
487
|
const built = await build(output, [grafanaSerializer]);
|
|
393
488
|
expect(built.errors).toEqual([]);
|
|
394
489
|
const text = (built.outputs.get("grafana") as SerializerResult).files!["dashboards/chant-fx-checkout.json"];
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* provenance and license in its README).
|
|
12
12
|
* - Alerting provisioning files (test/fixtures/alerting/).
|
|
13
13
|
* - What this lexicon's examples build.
|
|
14
|
+
* - Datasource provisioning files (test/fixtures/provisioning/).
|
|
14
15
|
*/
|
|
15
16
|
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
16
17
|
import { dirname, join, resolve } from "path";
|
|
@@ -58,6 +59,13 @@ export const KUBE_PROMETHEUS: readonly string[] = readdirSync(join(fixturesDir,
|
|
|
58
59
|
.sort()
|
|
59
60
|
.map((f) => `kube-prometheus/${f}`);
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Datasource provisioning files (test/fixtures/provisioning/, provenance in
|
|
64
|
+
* its README): one written for chant with every plugin whose settings are
|
|
65
|
+
* typed, and grafana/docker-otel-lgtm's.
|
|
66
|
+
*/
|
|
67
|
+
export const PROVISIONING: readonly string[] = ["provisioning/typed-plugins.datasources.yaml", "provisioning/docker-otel-lgtm.datasources.yaml"];
|
|
68
|
+
|
|
61
69
|
/**
|
|
62
70
|
* The alerting provisioning corpus (test/fixtures/alerting/, provenance and
|
|
63
71
|
* licenses in its README): Grafana 12.4.11 and 13.2.2 exports, and files
|
package/src/import/v2.ts
CHANGED
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
* pin bump that adds a v2 key fails until the key is placed in one of them.
|
|
21
21
|
*
|
|
22
22
|
* What the classic model can hold but chant cannot yet (library panels,
|
|
23
|
-
* annotations
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* annotations) is written to the classic JSON as Grafana would and reported
|
|
24
|
+
* by the classic importer (`./parser.ts`), as for any classic dashboard. Ad
|
|
25
|
+
* hoc, group by and switch variables are written the same way and imported
|
|
26
|
+
* by the classic importer's variable mappings (`./mappings.ts`).
|
|
26
27
|
*
|
|
27
28
|
* A classic read of a dashboard Grafana stores as v2 is the lossy direction:
|
|
28
29
|
* `storedAsV2` recognises it from the resource's
|