@intentius/chant-lexicon-prometheus 0.101.0 → 0.102.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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dist/alertmanager.d.ts.map +1 -1
  3. package/dist/composites/genai.d.ts +1 -1
  4. package/dist/composites/genai.d.ts.map +1 -1
  5. package/dist/composites/slo.d.ts +1 -1
  6. package/dist/composites/slo.d.ts.map +1 -1
  7. package/dist/import/embedded.d.ts +12 -3
  8. package/dist/import/embedded.d.ts.map +1 -1
  9. package/dist/import/generator.d.ts +14 -0
  10. package/dist/import/generator.d.ts.map +1 -1
  11. package/dist/import/parser.d.ts.map +1 -1
  12. package/dist/init-templates.d.ts.map +1 -1
  13. package/dist/integrations.d.ts.map +1 -1
  14. package/dist/integrity.json +7 -6
  15. package/dist/lint/audit-catalog.d.ts.map +1 -1
  16. package/dist/lint/audit-lineage.d.ts +33 -0
  17. package/dist/lint/audit-lineage.d.ts.map +1 -0
  18. package/dist/lint/post-synth/index.d.ts.map +1 -1
  19. package/dist/lint/post-synth/prom-helpers.d.ts +1 -1
  20. package/dist/lint/post-synth/prom208.d.ts +1 -1
  21. package/dist/lint/post-synth/prom209.d.ts +1 -1
  22. package/dist/lint/post-synth/prom210.d.ts +8 -0
  23. package/dist/lint/post-synth/prom210.d.ts.map +1 -0
  24. package/dist/manifest.json +1 -1
  25. package/dist/okf/index.md +1 -0
  26. package/dist/okf/rules/PROM210.md +11 -0
  27. package/dist/rule-eval.d.ts.map +1 -1
  28. package/dist/rules/prom-helpers.ts +1 -1
  29. package/dist/rules/prom208.ts +1 -1
  30. package/dist/rules/prom209.ts +1 -1
  31. package/dist/rules/prom210.ts +17 -0
  32. package/dist/skill-defs.d.ts +1 -1
  33. package/dist/skill-defs.d.ts.map +1 -1
  34. package/dist/skills/chant-prometheus-alertmanager.md +3 -1
  35. package/dist/validate-config.d.ts +2 -2
  36. package/dist/validate-config.d.ts.map +1 -1
  37. package/dist/validate-integrations.d.ts +37 -0
  38. package/dist/validate-integrations.d.ts.map +1 -0
  39. package/package.json +3 -3
  40. package/src/import/embedded.test.ts +52 -3
  41. package/src/import/embedded.ts +37 -5
  42. package/src/import/generator.ts +19 -1
  43. package/src/lint/audit-catalog.ts +13 -2
  44. package/src/lint/audit-lineage.ts +69 -0
  45. package/src/lint/post-synth/index.ts +2 -0
  46. package/src/lint/post-synth/post-synth.test.ts +34 -0
  47. package/src/lint/post-synth/prom-helpers.ts +1 -1
  48. package/src/lint/post-synth/prom208.ts +1 -1
  49. package/src/lint/post-synth/prom209.ts +1 -1
  50. package/src/lint/post-synth/prom210.ts +17 -0
  51. package/src/plugin.test.ts +1 -0
  52. package/src/plugin.ts +2 -2
  53. package/src/skills/chant-prometheus-alertmanager.md +3 -1
  54. package/src/testdata/integration-cases.ts +96 -0
  55. package/src/tools.test.ts +6 -0
  56. package/src/validate-config.ts +8 -31
  57. package/src/validate-integrations.ts +389 -0
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Receiver integration and `global:` checks for `alertmanager.yml`
3
+ * (PROM208-PROM210), one case per validation Alertmanager v0.34.1 makes when
4
+ * it loads the file (see ./pin.ts). Sources, under
5
+ * github.com/prometheus/alertmanager at that tag:
6
+ *
7
+ * - `config/notifiers.go`: the `UnmarshalYAML` of the Email, Slack (and its
8
+ * fields, actions and confirmations), WeChat, VictorOps, Pushover, SNS,
9
+ * Rocket.Chat and Webex configs.
10
+ * - `notify/<name>/config.go`: the `UnmarshalYAML` of the Discord,
11
+ * incident.io, Jira, Mattermost (and its fields), MS Teams, MS Teams v2,
12
+ * Opsgenie, PagerDuty, Telegram and webhook configs.
13
+ * - `config/config.go`: `Config.UnmarshalYAML`, which fills each
14
+ * integration from its `global:` default and then rejects what is still
15
+ * missing, and the `global:` "at most one of" checks; `HostPort` for
16
+ * `smarthost`.
17
+ *
18
+ * Durations come in two grammars. `model.Duration` fields (`resolve_timeout`,
19
+ * the route timers, Jira `reopen_duration`) use Prometheus's, see
20
+ * ./duration.ts. `time.Duration` fields (the `timeout` of webhook, Slack,
21
+ * PagerDuty and incident.io, and Pushover's `retry`, `expire` and `ttl`)
22
+ * use Go's `time.ParseDuration`: `1.5s` and `300ms` are valid, `1d` is not.
23
+ *
24
+ * Alertmanager stops at the first error; these checks report every one.
25
+ * PROM209 is something missing (a destination, credential or required
26
+ * field), PROM210 a setting Alertmanager rejects (two settings that exclude
27
+ * each other, or a value outside the allowed set), PROM208 a duration.
28
+ */
29
+
30
+ import { isValidDuration } from "./duration";
31
+ import type { AlertmanagerGlobalConfig, HttpClientConfig, ReceiverConfig } from "./model";
32
+ import type { PrometheusIssue } from "./validate-config";
33
+
34
+ type Obj = Record<string, unknown>;
35
+ type Code = "PROM208" | "PROM209" | "PROM210";
36
+
37
+ function str(v: unknown): string {
38
+ return typeof v === "string" ? v : String(v);
39
+ }
40
+
41
+ /** Set, as Go sees a string field: absent, null and "" all read as unset. */
42
+ function has(v: unknown): boolean {
43
+ return v !== undefined && v !== null && v !== "";
44
+ }
45
+
46
+ function asObj(v: unknown): Obj {
47
+ return v && typeof v === "object" ? (v as Obj) : {};
48
+ }
49
+
50
+ function asList(v: unknown): unknown[] {
51
+ return Array.isArray(v) ? v : [];
52
+ }
53
+
54
+ const GO_DURATION = /^[-+]?(?:(?:\d+\.?\d*|\.\d+)(?:ns|us|µs|μs|ms|s|m|h))+$/;
55
+
56
+ /** True when `value` is a Go `time.ParseDuration` duration (`0`, `1.5s`, `1h30m`, `300ms`). */
57
+ export function isValidGoDuration(value: unknown): boolean {
58
+ if (value === 0) return true;
59
+ if (typeof value !== "string") return false;
60
+ if (/^[-+]?0$/.test(value)) return true;
61
+ return GO_DURATION.test(value);
62
+ }
63
+
64
+ /**
65
+ * Whether an `http_config` carries an `Authorization` header once
66
+ * prometheus/common has read it: `bearer_token(_file)` becomes one.
67
+ */
68
+ function hasAuthorization(http: unknown): boolean {
69
+ const h = asObj(http) as HttpClientConfig;
70
+ return h.authorization !== undefined && h.authorization !== null ? true : has(h.bearer_token) || has(h.bearer_token_file);
71
+ }
72
+
73
+ /** `host:port`, as Go's `net.SplitHostPort` reads it, with a non-empty port. */
74
+ function isHostPort(v: unknown): boolean {
75
+ if (typeof v !== "string") return false;
76
+ const m = /^(?:\[[^\]]*\]|[^:[\]]*):([^:[\]]*)$/.exec(v);
77
+ return m !== null && m[1] !== "";
78
+ }
79
+
80
+ const SLACK_APP_URL = "https://slack.com/api/chat.postMessage";
81
+
82
+ class Reporter {
83
+ readonly issues: PrometheusIssue[] = [];
84
+ constructor(private readonly subject: string, private readonly prefix: string) {}
85
+ add(code: Code, message: string): void {
86
+ this.issues.push({ code, severity: "error", subject: this.subject, message: `${this.prefix}${message}` });
87
+ }
88
+ /** PROM210 when both a value and its `_file` sibling are set. */
89
+ oneOf(c: Obj, at: string, a: string, b = `${a}_file`): void {
90
+ if (has(c[a]) && has(c[b])) this.add("PROM210", `${at} sets both ${a} and ${b}; set at most one`);
91
+ }
92
+ goDuration(c: Obj, at: string, field: string): void {
93
+ const v = c[field];
94
+ if (v !== undefined && v !== null && !isValidGoDuration(v)) {
95
+ this.add("PROM208", `${at}.${field} "${str(v)}" is not a Go duration (e.g. 10s, 1m30s, 500ms)`);
96
+ }
97
+ }
98
+ promDuration(c: Obj, at: string, field: string): void {
99
+ const v = c[field];
100
+ if (v !== undefined && v !== null && !isValidDuration(v)) {
101
+ this.add("PROM208", `${at}.${field} "${str(v)}" is not a duration (e.g. 30m, 4h, 1d)`);
102
+ }
103
+ }
104
+ }
105
+
106
+ type IntegrationCheck = (c: Obj, at: string, g: Obj, r: Reporter) => void;
107
+
108
+ /** Webhook, Discord, MS Teams and MS Teams v2: a URL or its file, not both. */
109
+ function urlOrFile(field: string): IntegrationCheck {
110
+ return (c, at, _g, r) => {
111
+ if (!has(c[field]) && !has(c[`${field}_file`])) r.add("PROM209", `${at} has no ${field} or ${field}_file`);
112
+ r.oneOf(c, at, field);
113
+ };
114
+ }
115
+
116
+ /** Title and value on each field of a Slack or Mattermost message. */
117
+ function titledFields(c: Obj, at: string, r: Reporter): void {
118
+ asList(c.fields).forEach((f, j) => {
119
+ const field = asObj(f);
120
+ if (!has(field.title)) r.add("PROM210", `${at}.fields[${j}] has no title`);
121
+ if (!has(field.value)) r.add("PROM210", `${at}.fields[${j}] has no value`);
122
+ });
123
+ }
124
+
125
+ const OPSGENIE_RESPONDER_TYPES = ["team", "teams", "user", "escalation", "schedule"];
126
+ const VICTOROPS_RESERVED = ["routing_key", "message_type", "state_message", "entity_display_name", "monitoring_tool", "entity_id", "entity_state"];
127
+ const TELEGRAM_PARSE_MODES = ["Markdown", "MarkdownV2", "HTML"];
128
+ const JIRA_API_TYPES = ["auto", "cloud", "datacenter"];
129
+
130
+ /** Go's `textproto.CanonicalMIMEHeaderKey`, enough to compare header names. */
131
+ function canonicalHeader(name: string): string {
132
+ return name
133
+ .split("-")
134
+ .map((p) => (p ? p[0].toUpperCase() + p.slice(1).toLowerCase() : p))
135
+ .join("-");
136
+ }
137
+
138
+ const CHECKS: Record<string, IntegrationCheck> = {
139
+ // notify/discord/config.go; config/config.go
140
+ discord_configs: urlOrFile("webhook_url"),
141
+
142
+ // config/notifiers.go EmailConfig; config/config.go (global SMTP defaults, HostPort)
143
+ email_configs: (c, at, g, r) => {
144
+ if (!has(c.to)) r.add("PROM209", `${at} has no to address`);
145
+ if (!has(c.smarthost) && !has(g.smtp_smarthost)) r.add("PROM209", `${at} has no smarthost, and global sets no smtp_smarthost`);
146
+ if (!has(c.from) && !has(g.smtp_from)) r.add("PROM209", `${at} has no from address, and global sets no smtp_from`);
147
+ if (has(c.smarthost) && !isHostPort(c.smarthost)) r.add("PROM210", `${at}.smarthost "${str(c.smarthost)}" is not host:port`);
148
+ const seen = new Set<string>();
149
+ for (const h of Object.keys(asObj(c.headers))) {
150
+ const name = canonicalHeader(h);
151
+ if (seen.has(name)) r.add("PROM210", `${at}.headers sets ${name} more than once (header names are case-insensitive)`);
152
+ seen.add(name);
153
+ }
154
+ const threading = asObj(c.threading);
155
+ if (threading.enabled === true) {
156
+ for (const h of ["References", "In-Reply-To"]) {
157
+ if (seen.has(h)) r.add("PROM210", `${at} enables threading and sets a ${h} header; threading writes that header itself`);
158
+ }
159
+ if (threading.thread_by_date !== "none" && threading.thread_by_date !== "daily") {
160
+ r.add("PROM210", `${at}.threading.thread_by_date must be "none" or "daily" when threading is enabled`);
161
+ }
162
+ }
163
+ },
164
+
165
+ // notify/incidentio/config.go
166
+ incidentio_configs: (c, at, _g, r) => {
167
+ if (!has(c.url) && !has(c.url_file)) r.add("PROM209", `${at} has no url or url_file`);
168
+ r.oneOf(c, at, "url");
169
+ r.oneOf(c, at, "alert_source_token");
170
+ const token = has(c.alert_source_token) || has(c.alert_source_token_file);
171
+ const http = c.http_config !== undefined && c.http_config !== null;
172
+ if (http && hasAuthorization(c.http_config) && token) {
173
+ r.add("PROM210", `${at} sets alert_source_token(_file) and http_config.authorization; set one`);
174
+ }
175
+ // Alertmanager only asks for a credential when http_config is present.
176
+ if (http && !hasAuthorization(c.http_config) && !token) {
177
+ r.add("PROM209", `${at} has no alert_source_token(_file) and its http_config has no authorization`);
178
+ }
179
+ r.goDuration(c, at, "timeout");
180
+ },
181
+
182
+ // notify/jira/config.go; config/config.go (global.jira_api_url, which has no default)
183
+ jira_configs: (c, at, g, r) => {
184
+ if (!has(c.project)) r.add("PROM209", `${at} has no project`);
185
+ if (!has(c.issue_type)) r.add("PROM209", `${at} has no issue_type`);
186
+ if (!has(c.api_url) && !has(g.jira_api_url)) r.add("PROM209", `${at} has no api_url, and global sets no jira_api_url`);
187
+ if (has(c.api_type) && !JIRA_API_TYPES.includes(str(c.api_type))) {
188
+ r.add("PROM210", `${at}.api_type "${str(c.api_type)}" must be auto, cloud or datacenter`);
189
+ }
190
+ r.promDuration(c, at, "reopen_duration");
191
+ },
192
+
193
+ // notify/mattermost/config.go; config/config.go (global.mattermost_webhook_url)
194
+ mattermost_configs: (c, at, g, r) => {
195
+ if (!has(c.webhook_url) && !has(c.webhook_url_file) && !has(g.mattermost_webhook_url) && !has(g.mattermost_webhook_url_file)) {
196
+ r.add("PROM209", `${at} has no webhook_url or webhook_url_file, and global sets no mattermost_webhook_url(_file)`);
197
+ }
198
+ r.oneOf(c, at, "webhook_url");
199
+ titledFields(c, at, r);
200
+ asList(c.attachments).forEach((a, j) => titledFields(asObj(a), `${at}.attachments[${j}]`, r));
201
+ },
202
+
203
+ // notify/msteams/config.go, notify/msteamsv2/config.go
204
+ msteams_configs: urlOrFile("webhook_url"),
205
+ msteamsv2_configs: urlOrFile("webhook_url"),
206
+
207
+ // notify/opsgenie/config.go; config/config.go (global.opsgenie_api_key)
208
+ opsgenie_configs: (c, at, g, r) => {
209
+ if (!has(c.api_key) && !has(c.api_key_file) && !has(g.opsgenie_api_key) && !has(g.opsgenie_api_key_file)) {
210
+ r.add("PROM209", `${at} has no api_key or api_key_file, and global sets no opsgenie_api_key(_file)`);
211
+ }
212
+ r.oneOf(c, at, "api_key");
213
+ asList(c.responders).forEach((x, j) => {
214
+ const resp = asObj(x);
215
+ if (!has(resp.id) && !has(resp.username) && !has(resp.name)) {
216
+ r.add("PROM209", `${at}.responders[${j}] has none of id, username or name`);
217
+ }
218
+ const type = str(resp.type ?? "");
219
+ if (type === "") r.add("PROM209", `${at}.responders[${j}] has no type`);
220
+ else if (!type.includes("{{") && !OPSGENIE_RESPONDER_TYPES.includes(type.toLowerCase())) {
221
+ r.add("PROM210", `${at}.responders[${j}].type "${type}" must be team, teams, user, escalation or schedule`);
222
+ }
223
+ });
224
+ },
225
+
226
+ // notify/pagerduty/config.go
227
+ pagerduty_configs: (c, at, _g, r) => {
228
+ if (!has(c.routing_key) && !has(c.routing_key_file) && !has(c.service_key) && !has(c.service_key_file)) {
229
+ r.add("PROM209", `${at} has no routing_key(_file) or service_key(_file)`);
230
+ }
231
+ r.oneOf(c, at, "routing_key");
232
+ r.oneOf(c, at, "service_key");
233
+ r.goDuration(c, at, "timeout");
234
+ },
235
+
236
+ // config/notifiers.go PushoverConfig
237
+ pushover_configs: (c, at, _g, r) => {
238
+ if (!has(c.user_key) && !has(c.user_key_file)) r.add("PROM209", `${at} has no user_key or user_key_file`);
239
+ r.oneOf(c, at, "user_key");
240
+ if (!has(c.token) && !has(c.token_file)) r.add("PROM209", `${at} has no token or token_file`);
241
+ r.oneOf(c, at, "token");
242
+ if (c.html === true && c.monospace === true) r.add("PROM210", `${at} sets both html and monospace; set at most one`);
243
+ for (const f of ["retry", "expire", "ttl"]) r.goDuration(c, at, f);
244
+ },
245
+
246
+ // config/notifiers.go RocketchatConfig; config/config.go (global.rocketchat_token(_id))
247
+ rocketchat_configs: (c, at, g, r) => {
248
+ r.oneOf(c, at, "token");
249
+ r.oneOf(c, at, "token_id");
250
+ if (!has(c.token_id) && !has(c.token_id_file) && !has(g.rocketchat_token_id) && !has(g.rocketchat_token_id_file)) {
251
+ r.add("PROM209", `${at} has no token_id or token_id_file, and global sets no rocketchat_token_id(_file)`);
252
+ }
253
+ if (!has(c.token) && !has(c.token_file) && !has(g.rocketchat_token) && !has(g.rocketchat_token_file)) {
254
+ r.add("PROM209", `${at} has no token or token_file, and global sets no rocketchat_token(_file)`);
255
+ }
256
+ },
257
+
258
+ // config/notifiers.go SlackConfig, SlackField, SlackAction, SlackConfirmationField; config/config.go
259
+ slack_configs: (c, at, g, r) => {
260
+ r.oneOf(c, at, "api_url");
261
+ r.oneOf(c, at, "app_token");
262
+ const url = has(c.api_url) || has(c.api_url_file);
263
+ const token = has(c.app_token) || has(c.app_token_file);
264
+ const auth = hasAuthorization(c.http_config);
265
+ if (url && token) r.add("PROM210", `${at} sets both api_url(_file) and app_token(_file); set one`);
266
+ if (c.update_message === true && c.api_url !== SLACK_APP_URL) {
267
+ r.add("PROM210", `${at} sets update_message, which needs api_url set to ${SLACK_APP_URL} and the bot token in http_config.authorization`);
268
+ }
269
+ // The global app token only applies with no local authorization and no local api_url.
270
+ const globalToken = (has(g.slack_app_token) || has(g.slack_app_token_file)) && !auth && !url;
271
+ const globalUrl = has(g.slack_api_url) || has(g.slack_api_url_file);
272
+ if (!url && !token && !globalToken && !globalUrl) {
273
+ r.add("PROM209", `${at} has no api_url(_file) or app_token(_file), and global sets no slack_api_url(_file) or slack_app_token(_file)`);
274
+ }
275
+ if (token && auth) r.add("PROM210", `${at} sets app_token(_file) and http_config.authorization; the app token is the authorization`);
276
+ titledFields(c, at, r);
277
+ asList(c.actions).forEach((x, j) => {
278
+ const a = asObj(x);
279
+ if (!has(a.type)) r.add("PROM210", `${at}.actions[${j}] has no type`);
280
+ if (!has(a.text)) r.add("PROM210", `${at}.actions[${j}] has no text`);
281
+ if (!has(a.url) && !has(a.name)) r.add("PROM210", `${at}.actions[${j}] has no url or name`);
282
+ if (a.confirm !== undefined && a.confirm !== null && !has(asObj(a.confirm).text)) {
283
+ r.add("PROM210", `${at}.actions[${j}].confirm has no text`);
284
+ }
285
+ });
286
+ r.goDuration(c, at, "timeout");
287
+ },
288
+
289
+ // config/notifiers.go SNSConfig. Go's check is a chained XOR, so all three set passes; it is mirrored as is.
290
+ sns_configs: (c, at, _g, r) => {
291
+ const n = ["topic_arn", "phone_number", "target_arn"].filter((f) => has(c[f])).length;
292
+ if (n === 0) r.add("PROM209", `${at} has none of topic_arn, phone_number or target_arn`);
293
+ if (n === 2) r.add("PROM210", `${at} sets two of topic_arn, phone_number and target_arn; set one`);
294
+ },
295
+
296
+ // notify/telegram/config.go; config/config.go (global.telegram_bot_token)
297
+ telegram_configs: (c, at, g, r) => {
298
+ r.oneOf(c, at, "bot_token");
299
+ const chat = has(c.chat_id) && c.chat_id !== 0;
300
+ if (!chat && !has(c.chat_id_file)) r.add("PROM209", `${at} has no chat_id or chat_id_file`);
301
+ if (chat && has(c.chat_id_file)) r.add("PROM210", `${at} sets both chat_id and chat_id_file; set at most one`);
302
+ if (has(c.parse_mode) && !TELEGRAM_PARSE_MODES.includes(str(c.parse_mode))) {
303
+ r.add("PROM210", `${at}.parse_mode "${str(c.parse_mode)}" must be Markdown, MarkdownV2 or HTML`);
304
+ }
305
+ if (!has(c.bot_token) && !has(c.bot_token_file) && !has(g.telegram_bot_token) && !has(g.telegram_bot_token_file)) {
306
+ r.add("PROM209", `${at} has no bot_token or bot_token_file, and global sets no telegram_bot_token(_file)`);
307
+ }
308
+ },
309
+
310
+ // config/notifiers.go VictorOpsConfig; config/config.go (global.victorops_api_key)
311
+ victorops_configs: (c, at, g, r) => {
312
+ if (!has(c.routing_key)) r.add("PROM209", `${at} has no routing_key`);
313
+ r.oneOf(c, at, "api_key");
314
+ for (const f of Object.keys(asObj(c.custom_fields))) {
315
+ if (VICTOROPS_RESERVED.includes(f)) r.add("PROM210", `${at}.custom_fields sets ${f}, which VictorOps reserves`);
316
+ }
317
+ if (!has(c.api_key) && !has(c.api_key_file) && !has(g.victorops_api_key) && !has(g.victorops_api_key_file)) {
318
+ r.add("PROM209", `${at} has no api_key or api_key_file, and global sets no victorops_api_key(_file)`);
319
+ }
320
+ },
321
+
322
+ // config/notifiers.go WebexConfig. The authorization must be on the integration: the global http_config is applied after the check.
323
+ webex_configs: (c, at, _g, r) => {
324
+ if (!has(c.room_id)) r.add("PROM209", `${at} has no room_id`);
325
+ if (!hasAuthorization(c.http_config)) r.add("PROM209", `${at} has no http_config.authorization (the bot token)`);
326
+ },
327
+
328
+ // notify/webhook/config.go
329
+ webhook_configs: (c, at, g, r) => {
330
+ urlOrFile("url")(c, at, g, r);
331
+ r.goDuration(c, at, "timeout");
332
+ },
333
+
334
+ // config/notifiers.go WechatConfig; config/config.go (global.wechat_api_secret, global.wechat_api_corp_id)
335
+ wechat_configs: (c, at, g, r) => {
336
+ if (has(c.message_type) && c.message_type !== "text" && c.message_type !== "markdown") {
337
+ r.add("PROM210", `${at}.message_type "${str(c.message_type)}" must be text or markdown`);
338
+ }
339
+ r.oneOf(c, at, "api_secret");
340
+ if (!has(c.api_secret) && !has(c.api_secret_file) && !has(g.wechat_api_secret) && !has(g.wechat_api_secret_file)) {
341
+ r.add("PROM209", `${at} has no api_secret or api_secret_file, and global sets no wechat_api_secret(_file)`);
342
+ }
343
+ if (!has(c.corp_id) && !has(g.wechat_api_corp_id)) r.add("PROM209", `${at} has no corp_id, and global sets no wechat_api_corp_id`);
344
+ },
345
+ };
346
+
347
+ /** PROM208-PROM210 findings for every integration of one receiver. */
348
+ export function validateReceiverIntegrations(receiver: ReceiverConfig, global: AlertmanagerGlobalConfig): PrometheusIssue[] {
349
+ const name = str(receiver?.name ?? "");
350
+ const r = new Reporter(name, `receiver "${name}" `);
351
+ const g = asObj(global);
352
+ const rec = receiver as unknown as Obj;
353
+ for (const [key, check] of Object.entries(CHECKS)) {
354
+ asList(rec[key]).forEach((entry, i) => check(asObj(entry), `${key}[${i}]`, g, r));
355
+ }
356
+ return r.issues;
357
+ }
358
+
359
+ const GLOBAL_FILE_PAIRS = [
360
+ "slack_app_token",
361
+ "slack_api_url",
362
+ "opsgenie_api_key",
363
+ "victorops_api_key",
364
+ "telegram_bot_token",
365
+ "smtp_auth_password",
366
+ "rocketchat_token",
367
+ "rocketchat_token_id",
368
+ "smtp_auth_secret",
369
+ "wechat_api_secret",
370
+ "mattermost_webhook_url",
371
+ ];
372
+
373
+ /** PROM208 and PROM210 findings for `global:` (config/config.go `Config.UnmarshalYAML`). */
374
+ export function validateGlobalSettings(global: AlertmanagerGlobalConfig): PrometheusIssue[] {
375
+ const g = asObj(global);
376
+ const r = new Reporter("global", "");
377
+ r.promDuration(g, "global", "resolve_timeout");
378
+ for (const f of GLOBAL_FILE_PAIRS) r.oneOf(g, "global", f);
379
+ const token = has(g.slack_app_token) || has(g.slack_app_token_file);
380
+ const url = has(g.slack_api_url) || has(g.slack_api_url_file);
381
+ // Kept working for configs that point slack_api_url at the app URL and add a token (alertmanager#2513).
382
+ if (token && url && g.slack_api_url !== (g.slack_app_url ?? SLACK_APP_URL)) {
383
+ r.add("PROM210", "global sets both slack_app_token(_file) and slack_api_url(_file); set one, or set slack_api_url to the slack_app_url");
384
+ }
385
+ if (has(g.smtp_smarthost) && !isHostPort(g.smtp_smarthost)) {
386
+ r.add("PROM210", `global.smtp_smarthost "${str(g.smtp_smarthost)}" is not host:port`);
387
+ }
388
+ return r.issues;
389
+ }