@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GRAF104: Two dashboards, datasources, panels, variables or queries share an id
|
|
2
|
+
* GRAF104: Two dashboards, datasources, folders, panels, variables or queries share an id
|
|
3
3
|
*
|
|
4
|
-
* Dashboard
|
|
4
|
+
* Dashboard, folder and datasource uids and datasource names must be unique across the build, panel ids and variable names within a dashboard, and refIds within a panel. Grafana keeps one of two dashboards with the same uid and silently drops the other.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
@@ -9,7 +9,7 @@ import { grafanaDiagnostics } from "./grafana-helpers";
|
|
|
9
9
|
|
|
10
10
|
export const graf104: PostSynthCheck = {
|
|
11
11
|
id: "GRAF104",
|
|
12
|
-
description: "Two dashboards, datasources, panels, variables or queries share an id",
|
|
12
|
+
description: "Two dashboards, datasources, folders, panels, variables or queries share an id",
|
|
13
13
|
|
|
14
14
|
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
15
15
|
return grafanaDiagnostics(ctx, "GRAF104");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GRAF106: A dashboard or datasource uid Grafana rejects, or a dashboard with no title
|
|
2
|
+
* GRAF106: A dashboard, folder or datasource uid Grafana rejects, or a dashboard with no title
|
|
3
3
|
*
|
|
4
|
-
* Grafana accepts uids of 1-40 letters, digits, - and _, and refuses to save a dashboard without a title.
|
|
4
|
+
* Grafana accepts uids of 1-40 letters, digits, - and _, refuses the folder uid general (its root), and refuses to save a dashboard without a title.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
@@ -9,7 +9,7 @@ import { grafanaDiagnostics } from "./grafana-helpers";
|
|
|
9
9
|
|
|
10
10
|
export const graf106: PostSynthCheck = {
|
|
11
11
|
id: "GRAF106",
|
|
12
|
-
description: "A dashboard or datasource uid Grafana rejects, or a dashboard with no title",
|
|
12
|
+
description: "A dashboard, folder or datasource uid Grafana rejects, or a dashboard with no title",
|
|
13
13
|
|
|
14
14
|
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
15
15
|
return grafanaDiagnostics(ctx, "GRAF106");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GRAF110: A panel or row repeats over a variable that cannot give it more than one value
|
|
3
|
+
*
|
|
4
|
+
* Grafana repeats a panel or row once per selected value of a query, custom, datasource or group by variable. Over an ad hoc, constant, interval, textbox or switch variable it shows the panel once and logs an error; over a query, custom or datasource variable with neither multi nor includeAll there is only ever one value. A repeat naming no variable is GRAF103.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { grafanaDiagnostics } from "./grafana-helpers";
|
|
9
|
+
|
|
10
|
+
export const graf110: PostSynthCheck = {
|
|
11
|
+
id: "GRAF110",
|
|
12
|
+
description: "A panel or row repeats over a variable that cannot give it more than one value",
|
|
13
|
+
|
|
14
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
15
|
+
return grafanaDiagnostics(ctx, "GRAF110");
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -33,10 +33,15 @@ function parse(text: string, name: string): unknown[] {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** The grafana serializer's primary output (build.ts `GrafanaIndex`), which lists the folders the build resolved. */
|
|
37
|
+
function isGrafanaIndex(doc: unknown): doc is { folders?: Array<{ uid: string; title: string; path?: string; parentUid?: string }> } {
|
|
38
|
+
return typeof doc === "object" && doc !== null && typeof (doc as { grafanaSchema?: unknown }).grafanaSchema === "string" && Array.isArray((doc as { dashboards?: unknown }).dashboards);
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
/** Every dashboard, provisioned datasource and dashboard provider and alerting provisioning file in the build's output, and every `ExternalDatasource` it declares. */
|
|
37
42
|
export function grafanaArtifacts(ctx: PostSynthContext): GrafanaArtifacts {
|
|
38
43
|
const externalDatasources = [...(ctx.entities?.values() ?? [])].filter(isExternalDatasource).map(externalDatasourceRecord);
|
|
39
|
-
const out: GrafanaArtifacts = { dashboards: [], datasources: [], externalDatasources, providers: [], alerting: [] };
|
|
44
|
+
const out: GrafanaArtifacts = { dashboards: [], datasources: [], externalDatasources, providers: [], alerting: [], folders: [], deleteDatasources: [] };
|
|
40
45
|
for (const [lexicon, output] of ctx.outputs) {
|
|
41
46
|
const texts: Array<[string, string]> =
|
|
42
47
|
typeof output === "string"
|
|
@@ -49,8 +54,13 @@ export function grafanaArtifacts(ctx: PostSynthContext): GrafanaArtifacts {
|
|
|
49
54
|
else if (looksLikeDashboardProvisioning(doc)) {
|
|
50
55
|
for (const p of doc.providers) if (p && typeof p === "object" && !Array.isArray(p)) out.providers!.push(p as Record<string, unknown>);
|
|
51
56
|
} else if (looksLikeAlertingProvisioning(doc)) out.alerting!.push({ source, json: doc });
|
|
52
|
-
else if (
|
|
53
|
-
for (const
|
|
57
|
+
else if (isGrafanaIndex(doc)) {
|
|
58
|
+
for (const f of doc.folders ?? []) if (f && typeof f.uid === "string" && typeof f.title === "string") out.folders!.push(f);
|
|
59
|
+
} else if (looksLikeDatasourceProvisioning(doc)) {
|
|
60
|
+
for (const d of doc.deleteDatasources ?? []) {
|
|
61
|
+
if (d && typeof d === "object" && typeof (d as { name?: unknown }).name === "string") out.deleteDatasources!.push(d as { name: string; orgId?: number });
|
|
62
|
+
}
|
|
63
|
+
for (const d of doc.datasources ?? []) {
|
|
54
64
|
if (d && typeof d === "object" && typeof (d as ProvisionedDatasource).name === "string") {
|
|
55
65
|
const ds = d as ProvisionedDatasource;
|
|
56
66
|
out.datasources.push({ ...ds, uid: typeof ds.uid === "string" ? ds.uid : ds.name });
|
|
@@ -9,6 +9,7 @@ import { graf106 } from "./graf106";
|
|
|
9
9
|
import { graf107 } from "./graf107";
|
|
10
10
|
import { graf108 } from "./graf108";
|
|
11
11
|
import { graf109 } from "./graf109";
|
|
12
|
+
import { graf110 } from "./graf110";
|
|
12
13
|
import { graf111 } from "./graf111";
|
|
13
14
|
import { graf112 } from "./graf112";
|
|
14
15
|
import { graf113 } from "./graf113";
|
|
@@ -25,6 +26,7 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
25
26
|
graf107,
|
|
26
27
|
graf108,
|
|
27
28
|
graf109,
|
|
29
|
+
graf110,
|
|
28
30
|
graf111,
|
|
29
31
|
graf112,
|
|
30
32
|
graf113,
|
|
@@ -6,11 +6,12 @@ import { makePostSynthCtxFromFiles, makePostSynthCtx } from "@intentius/chant-te
|
|
|
6
6
|
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
7
7
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
8
8
|
import { grafanaSerializer } from "../../serializer";
|
|
9
|
-
import { Datasource, ExternalDatasource } from "../../datasource";
|
|
9
|
+
import { Datasource, DatasourceProvisioning, ExternalDatasource } from "../../datasource";
|
|
10
|
+
import { Folder } from "../../folder";
|
|
10
11
|
import { Dashboard, DashboardProvider } from "../../dashboard";
|
|
11
12
|
import { HeatmapPanel, Row, StatPanel, TimeSeriesPanel, TablePanel, TextPanel } from "../../panels";
|
|
12
13
|
import { LokiQuery, PromQuery, TempoQuery } from "../../query";
|
|
13
|
-
import { DatasourceVariable, QueryVariable } from "../../variables";
|
|
14
|
+
import { AdhocVariable, CustomVariable, DatasourceVariable, GroupByVariable, IntervalVariable, QueryVariable, SwitchVariable } from "../../variables";
|
|
14
15
|
import { graf101 } from "./graf101";
|
|
15
16
|
import { graf102 } from "./graf102";
|
|
16
17
|
import { graf103 } from "./graf103";
|
|
@@ -20,6 +21,7 @@ import { graf106 } from "./graf106";
|
|
|
20
21
|
import { graf107 } from "./graf107";
|
|
21
22
|
import { graf108 } from "./graf108";
|
|
22
23
|
import { graf109 } from "./graf109";
|
|
24
|
+
import { graf110 } from "./graf110";
|
|
23
25
|
import { graf115 } from "./graf115";
|
|
24
26
|
import { panelUnits } from "../../validate-output";
|
|
25
27
|
import { panelsOf } from "../../datasource-refs";
|
|
@@ -194,6 +196,18 @@ describe("GRAF103: undeclared variables", () => {
|
|
|
194
196
|
});
|
|
195
197
|
});
|
|
196
198
|
|
|
199
|
+
describe("GRAF101, GRAF103 on ad hoc and group by variables", () => {
|
|
200
|
+
test("checks an ad hoc or group by variable's datasource like a query variable's", () => {
|
|
201
|
+
const d = dashboardJson([], [
|
|
202
|
+
{ type: "adhoc", name: "f", datasource: { type: "prometheus", uid: "gone" }, filters: [] },
|
|
203
|
+
{ type: "groupby", name: "by", datasource: { type: "prometheus", uid: "${nope}" } },
|
|
204
|
+
]);
|
|
205
|
+
const ctx = ctxOfJson(d, [{ name: "Prometheus", type: "prometheus", uid: "prom" }]);
|
|
206
|
+
expect(graf101.check(ctx).map((x) => x.message)).toEqual([expect.stringContaining('variable "f" uses datasource uid "gone"')]);
|
|
207
|
+
expect(graf103.check(ctx).map((x) => x.message)).toEqual([expect.stringContaining('variable "by" datasource uses $nope')]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
197
211
|
describe("GRAF104: duplicates", () => {
|
|
198
212
|
test("flags duplicate dashboard uids, datasource names and panel ids", () => {
|
|
199
213
|
const again = new Datasource({ name: "Prometheus", type: "prometheus", uid: "prom-2" });
|
|
@@ -363,6 +377,54 @@ describe("GRAF108: PromQL syntax", () => {
|
|
|
363
377
|
});
|
|
364
378
|
});
|
|
365
379
|
|
|
380
|
+
describe("GRAF110: repeats", () => {
|
|
381
|
+
test("flags a repeat over a variable Grafana cannot repeat over, and over one that holds one value", () => {
|
|
382
|
+
const ds = new DatasourceVariable({ name: "ds", pluginType: "prometheus" });
|
|
383
|
+
const w = new IntervalVariable({ name: "w", values: ["1m", "5m"] });
|
|
384
|
+
const on = new SwitchVariable({ name: "on" });
|
|
385
|
+
const f = new AdhocVariable({ name: "f", datasource: ds });
|
|
386
|
+
const env = new CustomVariable({ name: "env", values: ["a", "b"] });
|
|
387
|
+
const panels = [new StatPanel({ title: "by w", repeat: w }), new StatPanel({ title: "by on", repeat: on }), new StatPanel({ title: "by f", repeat: f }), new StatPanel({ title: "by env", repeat: env })];
|
|
388
|
+
const diags = graf110.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [ds, w, on, f, env], panels }) }));
|
|
389
|
+
expect(diags.map((x) => [x.checkId, x.severity])).toEqual(Array(4).fill(["GRAF110", "warning"]));
|
|
390
|
+
expect(diags.map((x) => x.message)).toEqual([
|
|
391
|
+
expect.stringContaining('panel "by w" (id 1) repeats over $w, an interval variable, which Grafana cannot repeat over'),
|
|
392
|
+
expect.stringContaining("repeats over $on, a switch variable"),
|
|
393
|
+
expect.stringContaining("repeats over $f, an ad hoc variable"),
|
|
394
|
+
expect.stringContaining("repeats over $env, a custom variable with neither multi nor includeAll"),
|
|
395
|
+
]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("checks a row's repeat too", () => {
|
|
399
|
+
const env = new CustomVariable({ name: "env", values: ["a", "b"] });
|
|
400
|
+
const row = new Row({ title: "Per env", repeat: env, panels: [new StatPanel({ title: "s" })] });
|
|
401
|
+
expect(graf110.check(ctxOf({ d: new Dashboard({ title: "D", variables: [env], panels: [row] }) })).map((x) => x.message)).toEqual([
|
|
402
|
+
expect.stringContaining('row "Per env" repeats over $env'),
|
|
403
|
+
]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("passes a multi-value or include-all variable, a group by variable, and leaves an undeclared one to GRAF103", () => {
|
|
407
|
+
const env = new CustomVariable({ name: "env", values: ["a", "b"], multi: true });
|
|
408
|
+
const job = new QueryVariable({ name: "job", datasource: prometheus, query: { query: "label_values(up, job)", qryType: 1 }, includeAll: true });
|
|
409
|
+
const ds = new DatasourceVariable({ name: "ds", pluginType: "prometheus", multi: true });
|
|
410
|
+
const by = new GroupByVariable({ name: "by", datasource: prometheus });
|
|
411
|
+
const panels = [env, job, ds, by].map((v) => new StatPanel({ title: v.variableName, repeat: v }));
|
|
412
|
+
panels.push(new StatPanel({ title: "gone", repeat: "gone" }));
|
|
413
|
+
expect(graf110.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [env, job, ds, by], panels }) }))).toEqual([]);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe("GRAF108 on object-form variable queries", () => {
|
|
418
|
+
test("parses the query text of an object query sent to Prometheus", () => {
|
|
419
|
+
const ds = new DatasourceVariable({ name: "ds", pluginType: "prometheus" });
|
|
420
|
+
const bad = new QueryVariable({ name: "bad", datasource: ds, query: { qryType: 1, query: 'label_values(up{job="x", instance)', refId: "PrometheusVariableQueryEditor-VariableQuery" } });
|
|
421
|
+
const good = new QueryVariable({ name: "good", datasource: ds, query: { qryType: 4, query: 'kube_pod_info{namespace=~"$ns"}' } });
|
|
422
|
+
const ns = new CustomVariable({ name: "ns", values: ["a"] });
|
|
423
|
+
const diags = graf108.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [ds, ns, bad, good] }) }));
|
|
424
|
+
expect(diags.map((x) => x.message)).toEqual([expect.stringContaining('variable "bad" query is not valid PromQL')]);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
366
428
|
describe("GRAF108 over real Grafana exports", () => {
|
|
367
429
|
const exportsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "test", "fixtures", "exports");
|
|
368
430
|
const files = readdirSync(exportsDir)
|
|
@@ -453,3 +515,38 @@ describe("GRAF115 over real Grafana exports and community dashboards", () => {
|
|
|
453
515
|
expect(unitsSeen).toBeGreaterThan(200);
|
|
454
516
|
});
|
|
455
517
|
});
|
|
518
|
+
|
|
519
|
+
describe("annotations, folders and datasource deletes (#2953)", () => {
|
|
520
|
+
const deploys = (datasource: unknown, extra: Record<string, unknown> = {}) => ({ name: "Deploys", datasource, ...extra }) as never;
|
|
521
|
+
|
|
522
|
+
test("GRAF101 flags an annotation on an undeclared datasource, GRAF102 one of another type", () => {
|
|
523
|
+
const d = new Dashboard({ title: "D", annotations: [deploys({ type: "prometheus", uid: "gone" })] });
|
|
524
|
+
expect(ids(graf101, ctxOf({ prometheus, d }))).toEqual([["GRAF101", "error"]]);
|
|
525
|
+
const t = new Dashboard({ title: "T", annotations: [deploys({ type: "loki", uid: "prometheus" })] });
|
|
526
|
+
expect(ids(graf102, ctxOf({ prometheus, t }))).toEqual([["GRAF102", "error"]]);
|
|
527
|
+
expect(ids(graf101, ctxOf({ prometheus, ok: new Dashboard({ title: "O", annotations: [deploys(prometheus)] }) }))).toEqual([]);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("GRAF101 flags an ExternalDatasource the provisioning file deletes", () => {
|
|
531
|
+
const old = new ExternalDatasource({ type: "prometheus", uid: "old", name: "Old" });
|
|
532
|
+
const d = new Dashboard({ title: "D", panels: [new StatPanel({ datasource: old, targets: [up] })] });
|
|
533
|
+
const diags = graf101.check(ctxOf({ prometheus, old, d, settings: new DatasourceProvisioning({ deleteDatasources: [{ name: "Old" }] }) }));
|
|
534
|
+
// The panel's own ref and its query's, as for any undeclared datasource.
|
|
535
|
+
expect(diags.map((x) => [x.checkId, x.severity])).toEqual([["GRAF101", "error"], ["GRAF101", "error"]]);
|
|
536
|
+
expect(diags[0].message).toContain("deleteDatasources");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("GRAF103 reads an annotation's query and datasource; GRAF108 parses its PromQL", () => {
|
|
540
|
+
const d = new Dashboard({ title: "D", annotations: [deploys(prometheus, { expr: "changes(up{job=\"$job\"}[5m] > 0" })] });
|
|
541
|
+
expect(ids(graf103, ctxOf({ prometheus, d }))).toEqual([["GRAF103", "error"]]);
|
|
542
|
+
expect(ids(graf108, ctxOf({ prometheus, d }))).toEqual([["GRAF108", "error"]]);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("GRAF104 flags two folders with one uid; GRAF106 a folder uid Grafana refuses", () => {
|
|
546
|
+
const two = ctxOf({ a: new Dashboard({ title: "A", folder: "Team A" }), b: new Dashboard({ title: "B", folder: "team-a" }) });
|
|
547
|
+
expect(ids(graf104, two)).toEqual([["GRAF104", "error"]]);
|
|
548
|
+
expect(ids(graf106, ctxOf({ g: new Dashboard({ title: "G", folder: "General" }) }))).toEqual([["GRAF106", "error"]]);
|
|
549
|
+
expect(ids(graf106, ctxOf({ f: new Folder({ title: "F", uid: "not a uid" }) }))).toEqual([["GRAF106", "error"]]);
|
|
550
|
+
expect(ids(graf106, ctxOf({ f: new Folder({ title: "F" }), d: new Dashboard({ title: "D", folder: "F/G" }) }))).toEqual([]);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importing the lexicon must not pay for schema validation (#2958): ajv
|
|
3
|
+
* loads the first time a schema is checked, and the schemas come from the
|
|
4
|
+
* generated `spec/schemas.gen.ts`, not from files beside the source.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import { dirname, join } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { SCHEMA_NAMES } from "./pin";
|
|
12
|
+
import { loadSchema } from "./spec/schemas";
|
|
13
|
+
import { bundledSchema, schemaValidationUnavailable } from "./schema-validate";
|
|
14
|
+
|
|
15
|
+
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const tsx = join(srcDir, "..", "..", "..", "node_modules", ".bin", "tsx");
|
|
17
|
+
|
|
18
|
+
/** How many ajv modules a fresh process has loaded after importing `entries`. */
|
|
19
|
+
function ajvModulesAfterImporting(...entries: string[]): number {
|
|
20
|
+
const imports = entries.map((e) => `import(${JSON.stringify(join(srcDir, e))})`).join(", ");
|
|
21
|
+
const script = `Promise.all([${imports}]).then(() => console.log(Object.keys(require.cache).filter((k) => k.includes("/ajv/")).length))`;
|
|
22
|
+
return Number(execFileSync(tsx, ["-e", script], { encoding: "utf-8" }).trim());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("load cost", () => {
|
|
26
|
+
test("importing the package root, its plugin or the validation subpath does not load ajv", () => {
|
|
27
|
+
// index.ts imports plugin.ts, whose post-synth checks reach schema-validate.ts.
|
|
28
|
+
expect(ajvModulesAfterImporting("index.ts", "validation.ts")).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("validation reads the overlaid schemas from the generated module", () => {
|
|
32
|
+
expect(schemaValidationUnavailable()).toBeUndefined();
|
|
33
|
+
for (const name of SCHEMA_NAMES) expect(bundledSchema(name)).toEqual(loadSchema(name));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Per image, one project is built and applied with `grafanaApply`, the Op
|
|
5
5
|
* activity, through its build output (the index and the dashboard files):
|
|
6
6
|
*
|
|
7
|
-
* 1. Apply:
|
|
8
|
-
*
|
|
7
|
+
* 1. Apply: three folders (one nested in another, with a pinned uid), a
|
|
8
|
+
* library panel and three dashboards are created, each dashboard and
|
|
9
|
+
* folder labelled with the project's marker.
|
|
9
10
|
* 2. Re-apply: nothing is created, and nothing is written.
|
|
10
11
|
* 3. A panel title is edited in the source and applied: that dashboard is
|
|
11
12
|
* updated, the rest unchanged.
|
|
@@ -43,13 +44,15 @@ const MARKER_LABELS = { "app.kubernetes.io/managed-by": "chant", "chant.intentiu
|
|
|
43
44
|
const LIBRARY_PANEL = { uid: "chant-e2e-burn", name: "Error budget burn", kind: 1, model: { type: "stat", title: "Error budget burn", description: "shared" } };
|
|
44
45
|
|
|
45
46
|
function source(opts: { requestsTitle: string; withErrors: boolean }): string {
|
|
46
|
-
return `import { Dashboard, StatPanel } from "@intentius/chant-lexicon-grafana";
|
|
47
|
+
return `import { Dashboard, Folder, StatPanel } from "@intentius/chant-lexicon-grafana";
|
|
47
48
|
|
|
48
49
|
export const requests = new StatPanel({ title: ${JSON.stringify(opts.requestsTitle)} });
|
|
49
50
|
export const latency = new StatPanel({ title: "Latency" });
|
|
50
51
|
export const fiveHundreds = new StatPanel({ title: "5xx" });
|
|
51
52
|
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
|
|
53
|
+
${opts.withErrors ? `export const errors = new Dashboard({ title: "Errors", folder: "Team B", panels: [fiveHundreds] });\n` : ""}export const teamA = new Folder({ title: "Team A" });
|
|
54
|
+
export const payments = new Folder({ title: "Payments", uid: "chant-e2e-payments", parent: teamA });
|
|
55
|
+
export const home = new Dashboard({ title: "Home", folder: payments });
|
|
53
56
|
`;
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -112,7 +115,7 @@ describe.skipIf(!hasDocker).each(GRAFANA_IMAGES)(`grafanaApply applies, re-appli
|
|
|
112
115
|
|
|
113
116
|
afterAll(() => scope.cleanup());
|
|
114
117
|
|
|
115
|
-
it("apply: creates the folders, the library panel and the dashboards, labelled with the marker", { timeout: 240_000 }, async () => {
|
|
118
|
+
it("apply: creates the folders, nested ones under their parent, the library panel and the dashboards, labelled with the marker", { timeout: 240_000 }, async () => {
|
|
116
119
|
await buildProject({ requestsTitle: "Requests", withErrors: true });
|
|
117
120
|
const out = await apply();
|
|
118
121
|
expect(out.api).toMatch(/^apis\/v1(beta1)?$/);
|
|
@@ -121,6 +124,7 @@ describe.skipIf(!hasDocker).each(GRAFANA_IMAGES)(`grafanaApply applies, re-appli
|
|
|
121
124
|
"created Dashboard/api-overview",
|
|
122
125
|
"created Dashboard/errors",
|
|
123
126
|
"created Dashboard/home",
|
|
127
|
+
"created Folder/chant-e2e-payments",
|
|
124
128
|
"created Folder/team-a",
|
|
125
129
|
"created Folder/team-b",
|
|
126
130
|
"created LibraryPanel/chant-e2e-burn",
|
|
@@ -133,6 +137,11 @@ describe.skipIf(!hasDocker).each(GRAFANA_IMAGES)(`grafanaApply applies, re-appli
|
|
|
133
137
|
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
138
|
expect(folder.metadata.labels).toMatchObject(MARKER_LABELS);
|
|
135
139
|
expect(folder.spec.title).toBe("Team A");
|
|
140
|
+
const nested = (await grafana.api("/api/folders/chant-e2e-payments")).body as { title: string; parentUid?: string; parents?: Array<{ uid: string }> };
|
|
141
|
+
expect(nested.title).toBe("Payments");
|
|
142
|
+
expect(nested.parentUid ?? nested.parents?.[0]?.uid).toBe("team-a");
|
|
143
|
+
const home = (await grafana.api(out.applied.find((a) => a.name === "home")!.address)).body as { metadata: { annotations: Record<string, string> } };
|
|
144
|
+
expect(home.metadata.annotations["grafana.app/folder"]).toBe("chant-e2e-payments");
|
|
136
145
|
const connections = (await grafana.api(`/api/library-elements/${LIBRARY_PANEL.uid}/connections`)).body as { result: Array<{ connectionUid: string }> };
|
|
137
146
|
expect(connections.result.map((c) => c.connectionUid)).toEqual(["api-overview"]);
|
|
138
147
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { afterAll, describe, expect, test } from "vitest";
|
|
5
5
|
import { describeApplyConformance } from "@intentius/chant-test-utils";
|
|
6
6
|
import { normalizeApply } from "@intentius/chant/apply";
|
|
7
|
-
import { grafanaApply, readBuiltDashboards, resolveMarker, toApplyResult, type GrafanaApplyDeps } from "./grafana-apply";
|
|
7
|
+
import { grafanaApply, readBuiltDashboards, readBuiltIndex, resolveMarker, toApplyResult, type GrafanaApplyDeps } from "./grafana-apply";
|
|
8
8
|
import { emptyGrafana, writableGrafana, type WritableGrafanaState } from "../../api/fake-grafana-writes";
|
|
9
9
|
import type { GrafanaHttp } from "../../api/client";
|
|
10
10
|
|
|
@@ -55,6 +55,20 @@ describe("readBuiltDashboards", () => {
|
|
|
55
55
|
]);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
test("reads the folders the index lists and each dashboard's folder uid (#2953)", () => {
|
|
59
|
+
const indexPath = buildOutput([{ json: dashboard("a"), folder: "Platform/Kubernetes" }]);
|
|
60
|
+
const index = JSON.parse(readFileSync(indexPath, "utf8")) as Json;
|
|
61
|
+
(index.dashboards as Json[])[0].folderUid = "k8s";
|
|
62
|
+
index.folders = [
|
|
63
|
+
{ uid: "plat", title: "Platform", path: "Platform" },
|
|
64
|
+
{ uid: "k8s", title: "Kubernetes", parentUid: "plat", path: "Platform/Kubernetes" },
|
|
65
|
+
];
|
|
66
|
+
writeFileSync(indexPath, JSON.stringify(index));
|
|
67
|
+
const built = readBuiltIndex(indexPath);
|
|
68
|
+
expect(built.dashboards).toEqual([{ json: dashboard("a"), folder: "Platform/Kubernetes", folderUid: "k8s" }]);
|
|
69
|
+
expect(built.folders.map((f) => [f.uid, f.parentUid])).toEqual([["plat", undefined], ["k8s", "plat"]]);
|
|
70
|
+
});
|
|
71
|
+
|
|
58
72
|
test("reads the grafana key of a combined multi-lexicon output", () => {
|
|
59
73
|
expect(readBuiltDashboards(buildOutput([{ json: dashboard("a") }], { combined: true }))).toEqual([{ json: dashboard("a") }]);
|
|
60
74
|
});
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { bindGrafana, GrafanaBindingError, type BindOptions } from "../../api/bind";
|
|
30
30
|
import type { GrafanaHttp } from "../../api/client";
|
|
31
31
|
import { applyGrafana, parseDashboardFile, planFromDashboards, planRefs, type BuiltDashboardInput, type GrafanaApplyOutcome } from "../../api/apply";
|
|
32
|
+
import type { FolderPlan } from "../../api/folders";
|
|
32
33
|
|
|
33
34
|
export type { GrafanaApplyOutcome } from "../../api/apply";
|
|
34
35
|
|
|
@@ -74,16 +75,40 @@ function isObject(v: unknown): v is Json {
|
|
|
74
75
|
* it. Pure but for the file reads.
|
|
75
76
|
*/
|
|
76
77
|
export function readBuiltDashboards(indexPath: string, read: (path: string) => string = (p) => readFileSync(p, "utf8")): BuiltDashboardInput[] {
|
|
78
|
+
return readBuiltIndex(indexPath, read).dashboards;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The grafana index inside a build output: the index itself, or its `grafana` key in a multi-lexicon output. */
|
|
82
|
+
function grafanaIndexOf(indexPath: string, read: (path: string) => string): Json {
|
|
77
83
|
const raw = parseDashboardFile(read(indexPath), indexPath);
|
|
78
84
|
const index = isObject(raw.grafana) && Array.isArray(raw.grafana.dashboards) ? raw.grafana : raw;
|
|
79
85
|
if (!Array.isArray(index.dashboards)) throw new Error(`grafana apply: ${indexPath} is not a grafana build index (it has no "dashboards" list)`);
|
|
86
|
+
return index;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The dashboards a build wrote, with their folders, and the folders its
|
|
91
|
+
* index lists (every level of every folder, `Folder`s holding no dashboard
|
|
92
|
+
* included). An index written before folders were listed has none, and the
|
|
93
|
+
* plan derives them from the dashboards' paths.
|
|
94
|
+
*/
|
|
95
|
+
export function readBuiltIndex(indexPath: string, read: (path: string) => string = (p) => readFileSync(p, "utf8")): { dashboards: BuiltDashboardInput[]; folders: FolderPlan[] } {
|
|
96
|
+
const index = grafanaIndexOf(indexPath, read);
|
|
80
97
|
const dir = dirname(indexPath);
|
|
81
|
-
|
|
98
|
+
const dashboards = (index.dashboards as unknown[]).map((entry, i) => {
|
|
82
99
|
if (!isObject(entry) || typeof entry.file !== "string") throw new Error(`grafana apply: ${indexPath} dashboards[${i}] names no file`);
|
|
83
100
|
const file = join(dir, entry.file);
|
|
84
101
|
const json = parseDashboardFile(read(file), file);
|
|
85
|
-
|
|
102
|
+
const folder = typeof entry.folder === "string" && entry.folder !== "" ? entry.folder : undefined;
|
|
103
|
+
const folderUid = folder && typeof entry.folderUid === "string" && entry.folderUid !== "" ? entry.folderUid : undefined;
|
|
104
|
+
return { json, ...(folder ? { folder } : {}), ...(folderUid ? { folderUid } : {}) };
|
|
86
105
|
});
|
|
106
|
+
const folders: FolderPlan[] = (Array.isArray(index.folders) ? index.folders : []).flatMap((f: unknown) =>
|
|
107
|
+
isObject(f) && typeof f.uid === "string" && typeof f.title === "string" && typeof f.path === "string"
|
|
108
|
+
? [{ uid: f.uid, title: f.title, path: f.path, ...(typeof f.parentUid === "string" ? { parentUid: f.parentUid } : {}) }]
|
|
109
|
+
: [],
|
|
110
|
+
);
|
|
111
|
+
return { dashboards, folders };
|
|
87
112
|
}
|
|
88
113
|
|
|
89
114
|
async function loadConfig(cwd: string): Promise<Pick<ChantConfig, "grafana" | "ownership"> | undefined> {
|
|
@@ -116,7 +141,8 @@ export function resolveMarker(args: Pick<GrafanaApplyArgs, "stack" | "ownershipE
|
|
|
116
141
|
* with the binding's reason.
|
|
117
142
|
*/
|
|
118
143
|
export async function grafanaApply(args: GrafanaApplyArgs, signal?: AbortSignal, deps: GrafanaApplyDeps = {}): Promise<GrafanaApplyOutcome> {
|
|
119
|
-
const
|
|
144
|
+
const built = readBuiltIndex(args.indexPath);
|
|
145
|
+
const plan = planFromDashboards(built.dashboards, built.folders);
|
|
120
146
|
const config = deps.config ?? (await loadConfig(args.cwd ?? process.cwd()));
|
|
121
147
|
const marker = resolveMarker(args, config);
|
|
122
148
|
const bind: BindOptions = {
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* API applier (`grafanaApply`, #2948) and its envelope projection
|
|
5
5
|
* (`toApplyResult`), which core's apply activity looks up by name.
|
|
6
6
|
*/
|
|
7
|
-
export { grafanaApply, toApplyResult, readBuiltDashboards, resolveMarker } from "./grafana-apply";
|
|
7
|
+
export { grafanaApply, toApplyResult, readBuiltDashboards, readBuiltIndex, resolveMarker } from "./grafana-apply";
|
|
8
8
|
export type { GrafanaApplyArgs, GrafanaApplyDeps, GrafanaApplyOutcome } from "./grafana-apply";
|
package/src/ownership.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from "@intentius/chant/ownership";
|
|
42
42
|
import { DASHBOARD_PROVIDER_TYPE } from "./dashboard";
|
|
43
43
|
import type { LiveDashboard } from "./api/dashboards";
|
|
44
|
+
import type { LiveFolder } from "./api/folders";
|
|
44
45
|
|
|
45
46
|
/** The label keys chant's marker uses on a `dashboard.grafana.app` resource. */
|
|
46
47
|
export const GRAFANA_OWNERSHIP_KEYS: ChannelKeys = LABEL_OWNERSHIP_KEYS;
|
|
@@ -101,3 +102,21 @@ export function dashboardOwnership(
|
|
|
101
102
|
}
|
|
102
103
|
return { ownership: "foreign" };
|
|
103
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A folder's ownership verdict, read the way a dashboard's is: chant's labels
|
|
108
|
+
* (the API applier's), else the manager annotations Grafana writes on a
|
|
109
|
+
* folder that file provisioning made for one of this project's providers.
|
|
110
|
+
* A folder read over `/api/folders` (Grafana 11) has neither: `unknown`.
|
|
111
|
+
*/
|
|
112
|
+
export function folderOwnership(folder: LiveFolder, providers: ReadonlySet<string>): { ownership: OwnershipVerdict; marker?: OwnershipMarker } {
|
|
113
|
+
if (folder.via === "legacy") return { ownership: "unknown" };
|
|
114
|
+
if (hasOwnershipMarker(folder.labels, GRAFANA_OWNERSHIP_KEYS)) {
|
|
115
|
+
const marker = readOwnership(folder.labels, GRAFANA_OWNERSHIP_KEYS);
|
|
116
|
+
return { ownership: classifyOwnership(folder.labels, GRAFANA_OWNERSHIP_KEYS), ...(marker ? { marker } : {}) };
|
|
117
|
+
}
|
|
118
|
+
const annotations = folder.annotations ?? {};
|
|
119
|
+
const manager = annotations[MANAGER_ID_ANNOTATION];
|
|
120
|
+
if (annotations[MANAGED_BY_ANNOTATION] === FILE_PROVISIONING_MANAGER && manager !== undefined && providers.has(manager)) return { ownership: "owned" };
|
|
121
|
+
return { ownership: "foreign" };
|
|
122
|
+
}
|
package/src/panels.test.ts
CHANGED
|
@@ -4,12 +4,14 @@ import { Dashboard } from "./dashboard";
|
|
|
4
4
|
import { renderDashboard } from "./build";
|
|
5
5
|
import { validateDashboardSchema } from "./schema-validate";
|
|
6
6
|
import { SCHEMA_NAMES } from "./pin";
|
|
7
|
+
import { registeredQueries } from "./query";
|
|
7
8
|
|
|
8
9
|
const builtins = panels.registeredPanels().filter((d) => d.builtin);
|
|
9
10
|
|
|
10
11
|
describe("built-in panels", () => {
|
|
11
12
|
test("every panel type Grafana ships with a schema at the pin has a class", () => {
|
|
12
|
-
const
|
|
13
|
+
const querySchemas = new Set<string>(registeredQueries().flatMap((d) => (d.schema ? [d.schema] : [])));
|
|
14
|
+
const panelSchemas = SCHEMA_NAMES.filter((n) => n !== "dashboard" && n !== "expr" && !querySchemas.has(n));
|
|
13
15
|
expect(builtins.map((d) => d.schema).filter(Boolean).sort()).toEqual([...panelSchemas].sort());
|
|
14
16
|
});
|
|
15
17
|
|
package/src/pin.ts
CHANGED
|
@@ -59,6 +59,12 @@ export const SCHEMA_NAMES = [
|
|
|
59
59
|
"prometheus",
|
|
60
60
|
"tempo",
|
|
61
61
|
"loki",
|
|
62
|
+
"elasticsearch",
|
|
63
|
+
"cloudwatch",
|
|
64
|
+
"azuremonitor",
|
|
65
|
+
"googlecloudmonitoring",
|
|
66
|
+
"bigquery",
|
|
67
|
+
"grafanapyroscope",
|
|
62
68
|
"expr",
|
|
63
69
|
] as const;
|
|
64
70
|
|
|
@@ -108,6 +114,12 @@ export const GRAFANA_SCHEMA_PIN: GrafanaSchemaPin = Object.freeze({
|
|
|
108
114
|
prometheus: "726fc97eeb1e37791dbbf988c3cf40de17bb8e623926e6979c4a3f947cb1af87",
|
|
109
115
|
tempo: "21aec4c333c9b8e5e9228e85a6225c76161975abb54002e6f12e919e3b4e54ae",
|
|
110
116
|
loki: "03b81d6b952e3d31785c4b52637e2b7b1c170cc2ea8ec7ec671c21599508e31b",
|
|
117
|
+
elasticsearch: "126b0e04116877f468df7474ad6f75167cf9c54feb125534211fbe1915e6421e",
|
|
118
|
+
cloudwatch: "06d50341c82420865f3f58bfd3e3081cd90fb625f1d275bb7ba0230033f9db3f",
|
|
119
|
+
azuremonitor: "fe0f2fc01ff626dec7ce17a0e3dbb2e76f51f20539c73bbded46bbc4c2d3e262",
|
|
120
|
+
googlecloudmonitoring: "e1f2e2086d4216f22aca84def440b6a31a76e074727aa2ea3d2d10671e68834b",
|
|
121
|
+
bigquery: "939a0a6aee2c98fd8f690659e17060ff2b5bad1b22437e3449741ce0acc5abe4",
|
|
122
|
+
grafanapyroscope: "297db04efc4f9501ffa729acd9e02e484998bc4d823c7da362e401faceed64a2",
|
|
111
123
|
expr: "b1f1c1e4f6bf00f0fb4cb32b5bcc1b0e91fad9b7ad9354ecb791322bd97a5564",
|
|
112
124
|
dashboardv2: "a2cfb8b731ff9f48c41f5aa06ade134cf0d93ab9f6f0567a34a4bc39a9fd984e",
|
|
113
125
|
}),
|
package/src/plugin.test.ts
CHANGED
|
@@ -16,7 +16,7 @@ describe("grafana plugin", () => {
|
|
|
16
16
|
|
|
17
17
|
it("ships lint rules and post-synth checks, all under the GRAF prefix", () => {
|
|
18
18
|
const ids = [...grafanaPlugin.lintRules!().map((r) => r.id), ...grafanaPlugin.postSynthChecks!().map((c) => c.id)];
|
|
19
|
-
expect(ids).toEqual(["GRAF001", "GRAF002", "GRAF101", "GRAF102", "GRAF103", "GRAF104", "GRAF105", "GRAF106", "GRAF107", "GRAF108", "GRAF109", "GRAF111", "GRAF112", "GRAF113", "GRAF114", "GRAF115"]);
|
|
19
|
+
expect(ids).toEqual(["GRAF001", "GRAF002", "GRAF101", "GRAF102", "GRAF103", "GRAF104", "GRAF105", "GRAF106", "GRAF107", "GRAF108", "GRAF109", "GRAF110", "GRAF111", "GRAF112", "GRAF113", "GRAF114", "GRAF115"]);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("catalogues every rule and check for chant audit, and nothing else", () => {
|
|
@@ -38,10 +38,17 @@ describe("grafana plugin", () => {
|
|
|
38
38
|
|
|
39
39
|
it("loads its skills with content", () => {
|
|
40
40
|
const skills = grafanaPlugin.skills!();
|
|
41
|
-
expect(skills.map((s) => s.name)).toEqual(["chant-grafana", "chant-grafana-provisioning", "chant-grafana-alerting"]);
|
|
41
|
+
expect(skills.map((s) => s.name)).toEqual(["chant-grafana", "chant-grafana-provisioning", "chant-grafana-alerting", "chant-grafana-operations"]);
|
|
42
42
|
for (const s of skills) expect(s.content.length).toBeGreaterThan(200);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it("offers a default init template and three named ones", () => {
|
|
46
|
+
const names = [undefined, "red", "k8s-pods", "slo"];
|
|
47
|
+
const sets = names.map((n) => grafanaPlugin.initTemplates!(n));
|
|
48
|
+
expect(new Set(sets).size).toBe(4);
|
|
49
|
+
for (const set of sets) expect(Object.keys(set.src).length).toBeGreaterThan(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
45
52
|
it("registers namespaced MCP contributions", async () => {
|
|
46
53
|
expect(grafanaPlugin.mcpTools!().map((t) => t.name)).toEqual(["grafana:diff"]);
|
|
47
54
|
const [resource] = grafanaPlugin.mcpResources!();
|
package/src/plugin.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { GrafanaParser } from "./import/parser";
|
|
|
14
14
|
import { GrafanaGenerator } from "./import/generator";
|
|
15
15
|
import { dashboardImporter } from "./import/embedded";
|
|
16
16
|
import { grafanaSkills } from "./skill-defs";
|
|
17
|
+
import { DEFAULT_TEMPLATE, K8S_PODS_TEMPLATE, RED_TEMPLATE, SLO_TEMPLATE } from "./init-templates";
|
|
17
18
|
import { BUILTIN_CATALOG } from "./catalog";
|
|
18
19
|
import { GRAFANA_SCHEMA_PIN } from "./pin";
|
|
19
20
|
import { compositeCatalog } from "./composites/catalog";
|
|
@@ -129,6 +130,14 @@ export const grafanaPlugin: LexiconPlugin = {
|
|
|
129
130
|
return [catalogResource];
|
|
130
131
|
},
|
|
131
132
|
|
|
133
|
+
// `chant init --lexicon grafana [--template red|k8s-pods|slo]`; see ./init-templates.ts.
|
|
134
|
+
initTemplates(template?: string) {
|
|
135
|
+
if (template === "red") return RED_TEMPLATE;
|
|
136
|
+
if (template === "k8s-pods") return K8S_PODS_TEMPLATE;
|
|
137
|
+
if (template === "slo") return SLO_TEMPLATE;
|
|
138
|
+
return DEFAULT_TEMPLATE;
|
|
139
|
+
},
|
|
140
|
+
|
|
132
141
|
detectTemplate(data: unknown) {
|
|
133
142
|
return detectTemplate(data);
|
|
134
143
|
},
|
package/src/promql-check.ts
CHANGED
|
@@ -164,7 +164,11 @@ function usesOf(dashboard: Json, known: ReadonlyMap<string, KnownDatasource>, li
|
|
|
164
164
|
const expr = use.target?.expr;
|
|
165
165
|
if (typeof expr !== "string" || expr.trim() === "") continue;
|
|
166
166
|
out.push({ where: library ? `library panel "${library}" query ${String(use.target?.refId ?? "?")}` : use.where, expr });
|
|
167
|
-
} else if (use.kind === "
|
|
167
|
+
} else if (use.kind === "annotation" && use.annotation) {
|
|
168
|
+
// A Prometheus annotation keeps its query in `target.expr`, or at the top level when saved before Grafana 10.
|
|
169
|
+
const expr = use.target?.expr ?? use.annotation.expr;
|
|
170
|
+
if (typeof expr === "string" && expr.trim() !== "") out.push({ where: `${use.where} query`, expr });
|
|
171
|
+
} else if (use.kind === "variable" && use.variable?.type === "query" && !library) {
|
|
168
172
|
const text = variableQueryText(use.variable);
|
|
169
173
|
if (text) for (const expr of variablePromql(text)) out.push({ where: `${use.where} query`, expr });
|
|
170
174
|
}
|