@intentius/chant-lexicon-prometheus 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 +6 -1
- package/dist/alertmanager.d.ts +4 -2
- package/dist/alertmanager.d.ts.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +14 -0
- package/dist/import/embedded.d.ts.map +1 -0
- package/dist/import/generator.d.ts +50 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/parser.d.ts +52 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/import/slo.d.ts +15 -0
- package/dist/import/slo.d.ts.map +1 -0
- package/dist/integrations.d.ts +531 -0
- package/dist/integrations.d.ts.map +1 -0
- package/dist/integrity.json +7 -7
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/lint/rules/prom-ast.d.ts +8 -0
- package/dist/lint/rules/prom-ast.d.ts.map +1 -1
- package/dist/lint/rules/promql-literal.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/model.d.ts +77 -121
- package/dist/model.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/literal-credential.ts +52 -15
- package/dist/rules/prom-ast.ts +36 -0
- package/dist/rules/promql-literal.ts +15 -1
- package/dist/skills/chant-prometheus-alertmanager.md +4 -0
- package/dist/skills/chant-prometheus.md +11 -0
- package/package.json +2 -2
- package/src/alertmanager.ts +4 -1
- package/src/build.ts +2 -0
- package/src/codegen/docs.ts +5 -1
- package/src/import/cli.test.ts +98 -0
- package/src/import/embedded.test.ts +56 -0
- package/src/import/embedded.ts +44 -0
- package/src/import/generated-types.e2e.test.ts +77 -0
- package/src/import/generator.test.ts +138 -0
- package/src/import/generator.ts +578 -0
- package/src/import/parser.test.ts +133 -0
- package/src/import/parser.ts +365 -0
- package/src/import/roundtrip.test.ts +358 -0
- package/src/import/slo.ts +175 -0
- package/src/import/testdata/alertmanager-full.yml +89 -0
- package/src/import/testdata/alertmanager-integrations.yml +215 -0
- package/src/import/testdata/fixtures.ts +122 -0
- package/src/import/testdata/rules-full.yml +42 -0
- package/src/import/testdata/upstream/alertmanager-conf-good.yml +120 -0
- package/src/import/testdata/upstream/alertmanager-mattermost-default-webhook-url-file.yml +16 -0
- package/src/import/testdata/upstream/alertmanager-opsgenie-default-apikey-file.yml +27 -0
- package/src/import/testdata/upstream/alertmanager-rocketchat-default-token-file.yml +25 -0
- package/src/import/testdata/upstream/alertmanager-route-labels.yml +65 -0
- package/src/import/testdata/upstream/alertmanager-simple.yml +130 -0
- package/src/import/testdata/upstream/alertmanager-sns-topic-arn.yml +17 -0
- package/src/import/testdata/upstream/alertmanager-telegram-default-bot-token-file.yml +16 -0
- package/src/import/testdata/upstream/alertmanager-victorops-default-apikey-file.yml +23 -0
- package/src/import/testdata/upstream/alertmanager-wechat-default-api-secret-file.yml +18 -0
- package/src/import/testdata/upstream/prometheus-alerting-rules.yml +15 -0
- package/src/import/testdata/upstream/prometheus-alerting-templates.yml +23 -0
- package/src/import/testdata/upstream/prometheus-recording-rules.yml +7 -0
- package/src/integrations.ts +538 -0
- package/src/lint/rules/literal-credential.ts +52 -15
- package/src/lint/rules/prom-ast.ts +36 -0
- package/src/lint/rules/promql-literal.ts +15 -1
- package/src/lint/rules/rules.test.ts +58 -0
- package/src/model.ts +157 -109
- package/src/plugin.ts +15 -0
- package/src/skills/chant-prometheus-alertmanager.md +4 -0
- package/src/skills/chant-prometheus.md +11 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant import <file>` end to end through core's import command: detection
|
|
3
|
+
* from a project that lists the prometheus lexicon, `--lexicon prometheus`
|
|
4
|
+
* without one, the files written, and `chant build` on them giving the file
|
|
5
|
+
* back.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
9
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { load } from "js-yaml";
|
|
12
|
+
import { build } from "@intentius/chant/build";
|
|
13
|
+
import { importCommand } from "@intentius/chant/cli/commands/import";
|
|
14
|
+
import { prometheusSerializer } from "../serializer";
|
|
15
|
+
import { builtFiles, exampleOutputs, pkgDir, read } from "./testdata/fixtures";
|
|
16
|
+
|
|
17
|
+
let dir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
dir = mkdtempSync(join(pkgDir, ".roundtrip-tmp-"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function write(name: string, content: string): string {
|
|
28
|
+
const path = join(dir, name);
|
|
29
|
+
writeFileSync(path, content);
|
|
30
|
+
return path;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function rebuilt(srcDir: string): Promise<{ rules?: string; alertmanager?: string }> {
|
|
34
|
+
const result = await build(srcDir, [prometheusSerializer]);
|
|
35
|
+
expect(result.errors).toEqual([]);
|
|
36
|
+
return builtFiles(result.outputs.get("prometheus"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("chant import", () => {
|
|
40
|
+
test("detects a rule file in a project that lists prometheus", async () => {
|
|
41
|
+
writeFileSync(join(dir, "chant.config.json"), JSON.stringify({ lexicons: ["prometheus"] }));
|
|
42
|
+
const templatePath = write("rules.yml", read("rules-full.yml"));
|
|
43
|
+
const output = join(dir, "src");
|
|
44
|
+
|
|
45
|
+
const result = await importCommand({ templatePath, output });
|
|
46
|
+
|
|
47
|
+
expect(result.error).toBeUndefined();
|
|
48
|
+
expect(result.success).toBe(true);
|
|
49
|
+
expect(result.lexicon).toBe("prometheus");
|
|
50
|
+
expect(result.generatedFiles).toEqual(["rules.ts"]);
|
|
51
|
+
expect(readFileSync(join(output, "rules.ts"), "utf-8")).toContain("new RuleGroup(");
|
|
52
|
+
const { rules } = await rebuilt(output);
|
|
53
|
+
expect(load(rules!)).toEqual(expect.objectContaining({ groups: expect.any(Array) }));
|
|
54
|
+
}, 30_000);
|
|
55
|
+
|
|
56
|
+
test("detects an alertmanager.yml in a project that lists prometheus", async () => {
|
|
57
|
+
writeFileSync(join(dir, "chant.config.json"), JSON.stringify({ lexicons: ["prometheus"] }));
|
|
58
|
+
const am = (await exampleOutputs()).find((o) => o.name === "alerting/alertmanager.yml")!;
|
|
59
|
+
const templatePath = write("alertmanager.yml", am.yaml);
|
|
60
|
+
const output = join(dir, "src");
|
|
61
|
+
|
|
62
|
+
const result = await importCommand({ templatePath, output });
|
|
63
|
+
|
|
64
|
+
expect(result.error).toBeUndefined();
|
|
65
|
+
expect(result.lexicon).toBe("prometheus");
|
|
66
|
+
expect(result.generatedFiles).toEqual(["receivers.ts", "time-intervals.ts", "routes.ts", "inhibit-rules.ts", "settings.ts"]);
|
|
67
|
+
expect((await rebuilt(output)).alertmanager).toBe(am.yaml);
|
|
68
|
+
}, 30_000);
|
|
69
|
+
|
|
70
|
+
test("--lexicon prometheus imports without a project, and prints what it rewrote", async () => {
|
|
71
|
+
const templatePath = write("alertmanager.yml", read("alertmanager-full.yml"));
|
|
72
|
+
const output = join(dir, "src");
|
|
73
|
+
|
|
74
|
+
// Without a project, detection tries every installed lexicon (#2965).
|
|
75
|
+
const detected = await importCommand({ templatePath, output: join(dir, "detected") });
|
|
76
|
+
expect(detected.error).toBeUndefined();
|
|
77
|
+
expect(detected.lexicon).toBe("prometheus");
|
|
78
|
+
expect(detected.detected).toBe(true);
|
|
79
|
+
|
|
80
|
+
const result = await importCommand({ templatePath, output, lexicon: "prometheus" });
|
|
81
|
+
|
|
82
|
+
expect(result.error).toBeUndefined();
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
expect(result.lexicon).toBe("prometheus");
|
|
85
|
+
expect(result.warnings.some((w) => w.includes("match/match_re are written as matchers"))).toBe(true);
|
|
86
|
+
const { alertmanager } = await rebuilt(output);
|
|
87
|
+
expect(alertmanager).toContain('- severity="page"');
|
|
88
|
+
expect(alertmanager).toContain("name: weekends");
|
|
89
|
+
}, 30_000);
|
|
90
|
+
|
|
91
|
+
test("--lexicon prometheus on a file that is neither names both shapes", async () => {
|
|
92
|
+
const templatePath = write("collector.yaml", "receivers:\n otlp: {}\nservice:\n pipelines: {}\n");
|
|
93
|
+
const result = await importCommand({ templatePath, output: join(dir, "src"), lexicon: "prometheus" });
|
|
94
|
+
expect(result.success).toBe(false);
|
|
95
|
+
expect(result.error).toContain("rule file");
|
|
96
|
+
expect(result.error).toContain("alertmanager.yml");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { embeddedDocument, type EmbeddedContent } from "@intentius/chant/import/embedded";
|
|
3
|
+
import { prometheusPlugin } from "../plugin";
|
|
4
|
+
import { ruleGroupsImporter } from "./embedded";
|
|
5
|
+
|
|
6
|
+
const GROUPS = [
|
|
7
|
+
{ name: "api", rules: [{ alert: "ApiDown", expr: "up{job=\"api\"} == 0", for: "5m" }] },
|
|
8
|
+
{ name: "recording", rules: [{ record: "job:up:sum", expr: "sum by (job) (up)" }] },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const RULE_FILE = `groups:
|
|
12
|
+
- name: api
|
|
13
|
+
rules:
|
|
14
|
+
- alert: ApiDown
|
|
15
|
+
expr: up{job="api"} == 0
|
|
16
|
+
for: 5m
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const site = (over: Partial<EmbeddedContent>): EmbeddedContent => ({
|
|
20
|
+
host: "k8s",
|
|
21
|
+
hostType: "K8s::Monitoring::PrometheusRule",
|
|
22
|
+
location: "PrometheusRule api spec.groups",
|
|
23
|
+
directory: "api",
|
|
24
|
+
...over,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("rule groups embedded in another lexicon's resource (#2962)", () => {
|
|
28
|
+
test("the plugin registers the importer", () => {
|
|
29
|
+
expect(prometheusPlugin.embeddedImporters?.()).toEqual([ruleGroupsImporter]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("matches a PrometheusRule's groups and a rule file held as text; not an alertmanager.yml", () => {
|
|
33
|
+
expect(ruleGroupsImporter.matches(site({ document: { groups: GROUPS }, select: "groups" }))).toBe(true);
|
|
34
|
+
expect(ruleGroupsImporter.matches(site({ text: RULE_FILE, document: embeddedDocument(RULE_FILE) }))).toBe(true);
|
|
35
|
+
const am = "route:\n receiver: x\nreceivers:\n - name: x\n";
|
|
36
|
+
expect(ruleGroupsImporter.matches(site({ text: am, document: embeddedDocument(am) }))).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("spec.groups becomes the list of declared groups, in the source's order", () => {
|
|
40
|
+
const out = ruleGroupsImporter.import(site({ document: { groups: GROUPS }, select: "groups" }));
|
|
41
|
+
expect(out.files.map((f) => f.path)).toEqual(["rules.ts"]);
|
|
42
|
+
expect(out.value).toEqual({
|
|
43
|
+
bindings: [
|
|
44
|
+
{ from: "rules.ts", name: "api" },
|
|
45
|
+
{ from: "rules.ts", name: "recording" },
|
|
46
|
+
],
|
|
47
|
+
shape: "list",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a rule file held as text goes through ruleFileYaml", () => {
|
|
52
|
+
const out = ruleGroupsImporter.import(site({ text: RULE_FILE, document: embeddedDocument(RULE_FILE) }));
|
|
53
|
+
expect(out.value.through).toEqual({ from: "@intentius/chant-lexicon-prometheus", name: "ruleFileYaml" });
|
|
54
|
+
expect(out.value.bindings).toEqual([{ from: "rules.ts", name: "api" }]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule groups embedded in another lexicon's resource, for `chant import`
|
|
3
|
+
* (#2962): a k8s `PrometheusRule`'s `spec.groups`, or a rule file held as
|
|
4
|
+
* text in a ConfigMap.
|
|
5
|
+
*
|
|
6
|
+
* The groups are imported exactly as `chant import rules.yml` would import
|
|
7
|
+
* them, into a directory of its own. `spec.groups` becomes the list of the
|
|
8
|
+
* declared groups (an `Slo`'s by its `rules` member), which the k8s
|
|
9
|
+
* serializer renders as the same groups; a rule file becomes
|
|
10
|
+
* `ruleFileYaml([...])`, the text the prometheus serializer writes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { EmbeddedContentImporter, EmbeddedImport } from "@intentius/chant/import/embedded";
|
|
14
|
+
import { looksLikeRuleFile } from "../model";
|
|
15
|
+
import { parsePrometheusYaml } from "./parser";
|
|
16
|
+
import { generateRuleFile } from "./generator";
|
|
17
|
+
|
|
18
|
+
const PACKAGE = "@intentius/chant-lexicon-prometheus";
|
|
19
|
+
|
|
20
|
+
export const ruleGroupsImporter: EmbeddedContentImporter = {
|
|
21
|
+
what: "Prometheus rule groups",
|
|
22
|
+
|
|
23
|
+
matches(content) {
|
|
24
|
+
if (!looksLikeRuleFile(content.document)) return false;
|
|
25
|
+
return content.select === "groups" || (content.select === undefined && typeof content.text === "string");
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
import(content): EmbeddedImport {
|
|
29
|
+
// JSON is YAML: the document goes through the same parser a rule file does.
|
|
30
|
+
const source = content.select === undefined ? content.text! : JSON.stringify(content.document);
|
|
31
|
+
const parsed = parsePrometheusYaml(source);
|
|
32
|
+
if (parsed.kind !== "rules") throw new Error("this is not a Prometheus rule file");
|
|
33
|
+
const { files, groups } = generateRuleFile(parsed.file);
|
|
34
|
+
const bindings = groups.map((g) => ({ from: g.path, name: g.name, ...(g.member ? { member: g.member } : {}) }));
|
|
35
|
+
return {
|
|
36
|
+
files,
|
|
37
|
+
value:
|
|
38
|
+
content.select === undefined
|
|
39
|
+
? { bindings, shape: "list", through: { from: PACKAGE, name: "ruleFileYaml" } }
|
|
40
|
+
: { bindings, shape: "list" },
|
|
41
|
+
warnings: parsed.warnings,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated source type-checks against the lexicon's types. Compiling
|
|
3
|
+
* the generated projects takes longer than the unit-test budget allows, so
|
|
4
|
+
* this runs with the e2e tests.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import * as ts from "typescript";
|
|
10
|
+
import { PrometheusParser } from "./parser";
|
|
11
|
+
import { PrometheusGenerator } from "./generator";
|
|
12
|
+
import { exampleOutputs, pkgDir, read, repoRoot, sloOutputs, UPSTREAM } from "./testdata/fixtures";
|
|
13
|
+
|
|
14
|
+
type Files = Array<{ path: string; content: string }>;
|
|
15
|
+
|
|
16
|
+
/** Type errors in generated projects, compiled together against the lexicon's source with the repo's compiler options. */
|
|
17
|
+
function typeErrors(projects: Record<string, Files>): string[] {
|
|
18
|
+
const dir = mkdtempSync(join(pkgDir, ".roundtrip-tmp-"));
|
|
19
|
+
try {
|
|
20
|
+
const files: string[] = [];
|
|
21
|
+
for (const [name, generated] of Object.entries(projects)) {
|
|
22
|
+
const sub = join(dir, name.replace(/[^A-Za-z0-9]+/g, "-"));
|
|
23
|
+
mkdirSync(sub);
|
|
24
|
+
for (const f of generated) {
|
|
25
|
+
writeFileSync(join(sub, f.path), f.content);
|
|
26
|
+
files.push(join(sub, f.path));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const configFile = ts.readConfigFile(join(repoRoot, "tsconfig.json"), ts.sys.readFile);
|
|
30
|
+
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, repoRoot);
|
|
31
|
+
const program = ts.createProgram(files, { ...parsed.options, noEmit: true });
|
|
32
|
+
const real = (f: string) => ts.sys.realpath?.(f) ?? f;
|
|
33
|
+
const fileSet = new Set(files.map(real));
|
|
34
|
+
const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => d.file && fileSet.has(real(d.file.fileName)));
|
|
35
|
+
// A program that saw none of the files would pass vacuously.
|
|
36
|
+
expect(files.every((f) => program.getSourceFile(f) !== undefined)).toBe(true);
|
|
37
|
+
return diagnostics.map((d) => {
|
|
38
|
+
const at = real(d.file!.fileName).slice(real(dir).length + 1);
|
|
39
|
+
return `${at}: ${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`;
|
|
40
|
+
});
|
|
41
|
+
} finally {
|
|
42
|
+
rmSync(dir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const generate = (yaml: string): Files => new PrometheusGenerator().generate(new PrometheusParser().parse(yaml));
|
|
47
|
+
|
|
48
|
+
describe("the generated source type-checks against the lexicon's types", () => {
|
|
49
|
+
test("for every fixture", async () => {
|
|
50
|
+
const projects: Record<string, Files> = {
|
|
51
|
+
rulesFull: generate(read("rules-full.yml")),
|
|
52
|
+
alertmanagerFull: generate(read("alertmanager-full.yml")),
|
|
53
|
+
alertmanagerIntegrations: generate(read("alertmanager-integrations.yml")),
|
|
54
|
+
...Object.fromEntries(UPSTREAM.map((f) => [f, generate(read("upstream", f))])),
|
|
55
|
+
...Object.fromEntries(sloOutputs().map((o) => [o.name, generate(o.yaml)])),
|
|
56
|
+
...Object.fromEntries((await exampleOutputs()).map((o) => [o.name, generate(o.yaml)])),
|
|
57
|
+
};
|
|
58
|
+
expect(typeErrors(projects)).toEqual([]);
|
|
59
|
+
}, 120_000);
|
|
60
|
+
|
|
61
|
+
test("a field outside a notifier's typed config is carried as found, and tsc points at it", () => {
|
|
62
|
+
const yaml = [
|
|
63
|
+
"route:",
|
|
64
|
+
" receiver: chat",
|
|
65
|
+
"receivers:",
|
|
66
|
+
" - name: chat",
|
|
67
|
+
" slack_configs:",
|
|
68
|
+
" - channel: '#alerts'",
|
|
69
|
+
" api_url_file: /etc/alertmanager/slack-url",
|
|
70
|
+
" message_text: '{{ .CommonLabels.alertname }}'",
|
|
71
|
+
" mesage_txt: typo",
|
|
72
|
+
"",
|
|
73
|
+
].join("\n");
|
|
74
|
+
const errors = typeErrors({ slack: generate(yaml) });
|
|
75
|
+
expect(errors).toEqual([expect.stringMatching(/^slack\/receivers\.ts: .*'mesage_txt' does not exist in type 'SlackConfig'/)]);
|
|
76
|
+
}, 120_000);
|
|
77
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { AlertmanagerConfig, RuleGroupConfig } from "../model";
|
|
3
|
+
import { generateAlertmanagerFiles, generateRuleFileFiles, PrometheusGenerator, tsLiteral } from "./generator";
|
|
4
|
+
|
|
5
|
+
const byPath = (files: Array<{ path: string; content: string }>) => Object.fromEntries(files.map((f) => [f.path, f.content]));
|
|
6
|
+
|
|
7
|
+
describe("rule files", () => {
|
|
8
|
+
test("a group is a Rule[] const of plain objects and a RuleGroup, exported by list", () => {
|
|
9
|
+
const files = byPath(
|
|
10
|
+
generateRuleFileFiles({
|
|
11
|
+
groups: [{ name: "api-slo", interval: "30s", labels: { team: "api" }, rules: [{ record: "a:b", expr: "sum(x)" }] }],
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
expect(files["rules.ts"]).toBe(
|
|
15
|
+
[
|
|
16
|
+
"/** Rule groups */",
|
|
17
|
+
'import { type LabelSet, type Rule, RuleGroup } from "@intentius/chant-lexicon-prometheus";',
|
|
18
|
+
"",
|
|
19
|
+
'const apiSloLabels: LabelSet = { team: "api" };',
|
|
20
|
+
'const apiSloRules: Rule[] = [{ record: "a:b", expr: "sum(x)" }];',
|
|
21
|
+
"const apiSlo = new RuleGroup({",
|
|
22
|
+
' name: "api-slo",',
|
|
23
|
+
' interval: "30s",',
|
|
24
|
+
" labels: apiSloLabels,",
|
|
25
|
+
" rules: apiSloRules,",
|
|
26
|
+
"});",
|
|
27
|
+
"",
|
|
28
|
+
"export { apiSlo };",
|
|
29
|
+
"",
|
|
30
|
+
].join("\n"),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("more than eight groups are split across files, the limit COR009 sets", () => {
|
|
35
|
+
const groups: RuleGroupConfig[] = Array.from({ length: 10 }, (_, i) => ({ name: `g${i}`, rules: [] }));
|
|
36
|
+
const files = generateRuleFileFiles({ groups });
|
|
37
|
+
expect(files.map((f) => f.path)).toEqual(["rules-1.ts", "rules-2.ts"]);
|
|
38
|
+
expect(files[0].content).toContain("export { g0, g1, g2, g3, g4, g5, g6, g7 };");
|
|
39
|
+
expect(files[1].content).toContain("export { g8, g9 };");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("names that clash or are not identifiers get a suffix", () => {
|
|
43
|
+
const files = byPath(
|
|
44
|
+
generateRuleFileFiles({
|
|
45
|
+
groups: [
|
|
46
|
+
{ name: "default", rules: [] },
|
|
47
|
+
{ name: "2xx", rules: [] },
|
|
48
|
+
{ name: "Slo", rules: [] },
|
|
49
|
+
{ name: "a.b", rules: [] },
|
|
50
|
+
{ name: "a-b", rules: [] },
|
|
51
|
+
],
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
expect(files["rules.ts"]).toContain("export { defaultGroup, group2xx, slo, aB, aBGroup };");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("alertmanager.yml", () => {
|
|
59
|
+
const config: AlertmanagerConfig = {
|
|
60
|
+
route: {
|
|
61
|
+
receiver: "fallback",
|
|
62
|
+
routes: [
|
|
63
|
+
{ receiver: "pager", matchers: ['severity="page"'], mute_time_intervals: ["nights", "undeclared"] },
|
|
64
|
+
{ receiver: "elsewhere" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
receivers: [
|
|
68
|
+
{ name: "fallback" },
|
|
69
|
+
{ name: "pager", pagerduty_configs: [{ routing_key_file: "/k" }] },
|
|
70
|
+
],
|
|
71
|
+
time_intervals: [{ name: "nights", time_intervals: [{ times: [{ start_time: "00:00", end_time: "06:00" }] }] }],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
test("routes reference receivers and time intervals by variable; names nothing declares stay strings", () => {
|
|
75
|
+
const files = byPath(generateAlertmanagerFiles(config));
|
|
76
|
+
expect(Object.keys(files)).toEqual(["receivers.ts", "time-intervals.ts", "routes.ts"]);
|
|
77
|
+
expect(files["routes.ts"]).toContain('import { fallback, pager } from "./receivers";');
|
|
78
|
+
expect(files["routes.ts"]).toContain('import { nights } from "./time-intervals";');
|
|
79
|
+
expect(files["routes.ts"]).toContain('mute_time_intervals: [nights, "undeclared"]');
|
|
80
|
+
expect(files["routes.ts"]).toContain('{ receiver: "elsewhere" }');
|
|
81
|
+
expect(files["routes.ts"]).toContain("const root = new Route({ receiver: fallback, routes: rootChildren });");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("every integration Alertmanager defines is declared with its type", () => {
|
|
85
|
+
const files = byPath(
|
|
86
|
+
generateAlertmanagerFiles({
|
|
87
|
+
receivers: [{ name: "ops", opsgenie_configs: [{ api_key_file: "/k", priority: "P1" }], msteamsv2_configs: [{ webhook_url_file: "/t" }] }],
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
expect(files["receivers.ts"]).toContain('const opsOpsgenie: OpsGenieConfig[] = [{ api_key_file: "/k", priority: "P1" }];');
|
|
91
|
+
expect(files["receivers.ts"]).toContain('const opsMsteamsv2: MSTeamsV2Config[] = [{ webhook_url_file: "/t" }];');
|
|
92
|
+
expect(files["receivers.ts"]).not.toContain("Untyped");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a receiver or global key Alertmanager doesn't define is spread in from an untyped const, with a comment", () => {
|
|
96
|
+
const files = byPath(
|
|
97
|
+
generateAlertmanagerFiles({
|
|
98
|
+
global: { resolve_timeout: "5m", pigeon_loft: "roof" } as never,
|
|
99
|
+
receivers: [{ name: "ops", pigeon_configs: [{ loft: "roof" }] } as never],
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
expect(files["receivers.ts"]).toContain(
|
|
103
|
+
[
|
|
104
|
+
'// Receiver "ops": pigeon_configs is not a field Alertmanager v0.34.1 defines, so it is carried as',
|
|
105
|
+
"// data, untyped.",
|
|
106
|
+
'const opsUntyped = { pigeon_configs: [{ loft: "roof" }] };',
|
|
107
|
+
'const ops = new Receiver({ name: "ops", ...opsUntyped });',
|
|
108
|
+
].join("\n"),
|
|
109
|
+
);
|
|
110
|
+
expect(files["settings.ts"]).toContain("// global: pigeon_loft is not a field Alertmanager v0.34.1 defines");
|
|
111
|
+
expect(files["settings.ts"]).toContain("...globalUntyped,");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("tsLiteral", () => {
|
|
116
|
+
test("multi-line strings are template literals with backticks, backslashes and ${ escaped", () => {
|
|
117
|
+
expect(tsLiteral("a\n`b` \\d ${x}", 0)).toBe("`a\n\\`b\\` \\\\d \\${x}`");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("a string with double quotes is single-quoted, and keys that are not identifiers are quoted", () => {
|
|
121
|
+
expect(tsLiteral({ "a-b": 'x="1"', continue: true }, 0)).toBe(`{ "a-b": 'x="1"', continue: true }`);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("PrometheusGenerator never hands core an empty file list", () => {
|
|
126
|
+
expect(new PrometheusGenerator().generate({ resources: [], parameters: [] })).toHaveLength(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("an Slo's variable leaves its member's name, <name>Rules, free", async () => {
|
|
130
|
+
const { Slo } = await import("../composites/slo");
|
|
131
|
+
const { ruleGroupConfig } = await import("../rules");
|
|
132
|
+
const slo = ruleGroupConfig(
|
|
133
|
+
Slo({ name: "checkout", objective: 0.99, window: "30d", sli: { errors: "sum(rate(e[{{window}}]))", total: "sum(rate(t[{{window}}]))" } }).rules,
|
|
134
|
+
);
|
|
135
|
+
const files = byPath(generateRuleFileFiles({ groups: [{ name: "checkout-rules", rules: [] }, slo] }));
|
|
136
|
+
expect(files["slos.ts"]).toContain("const checkout = Slo({");
|
|
137
|
+
expect(files["rules.ts"]).toContain("const checkoutRulesGroup = new RuleGroup(");
|
|
138
|
+
});
|