Kubernetes & Mimir
Most Alertmanagers don't read a hand-written alertmanager.yml. They get their config from a
Kubernetes Secret, from kube-prometheus-stack Helm values, from prometheus-operator
AlertmanagerConfig resources, or from a Mimir or Cortex tenant config. amlint reads all of
these directly. check, tree, route, test and diff work the same on each, and
findings point at the line in the file you gave it, not at some config it generated.
amlint check alertmanager-secret.yaml
amlint check values.yaml
amlint check tenant-config.yaml
kubectl kustomize deploy/monitoring | amlint check -
amlint route alertmanagerconfigs.yaml namespace=team-a severity=critical
| input | what amlint reads | findings point at |
|---|---|---|
plain alertmanager.yml |
the file, as before | the line in the file |
Kubernetes Secret / ConfigMap |
alertmanager.yaml in stringData or base64 data, other keys as template files |
inside a \| block: the exact line; base64 or a quoted string: the alertmanager.yaml: key |
| kube-prometheus-stack values | alertmanager.config merged over the chart's default, alertmanager.templateFiles |
the line in the values file |
| Mimir / Cortex | alertmanager_config (a string) and template_files |
inside a \| block: the exact line, otherwise the key |
AlertmanagerConfig (v1alpha1, v1beta1) |
the resources, merged into the base config the way prometheus-operator merges them | the line in the resource |
Detection and --input-format
The format is detected from the content. A file can be a stream of YAML documents
(---) and can contain kind: List objects, as kubectl get -o yaml prints them. Other
Kubernetes objects in the stream (Deployments, Services, PrometheusRules) are skipped.
- An
AlertmanagerConfiganywhere in the stream makes it analertmanagerconfiginput. A Secret, values file, Mimir config or plain config in the same stream is the base config the resources are merged into. - A
SecretorConfigMapwith analertmanager.yaml,alertmanager.ymloralertmanager.yaml.gzkey is asecretinput. One without such a key is skipped. - A top-level
alertmanager_config:string is amimirinput. - A top-level
alertmanager:section, and no top-levelroute:orreceivers:, is ahelminput. Alertmanager rejects unknown top-level keys, so a plain config can't have one. - Anything else is a plain Alertmanager config, read exactly as amlint always read it.
--input-format (on check, diff, tree, route and test) overrides the detection:
auto (the default), alertmanager, secret, helm, mimir or alertmanagerconfig.
--input-format alertmanager reads the file as one plain document, even if it looks like
something else. A file that holds two Alertmanager configs (two config Secrets, say) exits 2:
amlint lints one config per file.
Notes about how a file was read, like which parts of a Helm config come from the chart's
defaults, go to stderr as note: lines and don't change the exit code.
Kubernetes Secrets
apiVersion: v1
kind: Secret
metadata:
name: alertmanager-main
namespace: monitoring
stringData:
alertmanager.yaml: |-
route:
receiver: default
...
notify.tmpl: |-
{{ define "notify.description" }}...{{ end }}
stringDatawins overdatafor the same key, as the API server merges them.datavalues are base64.alertmanager.yaml.gzis the gzipped config in the Secret the operator generates (alertmanager-<name>-generated).- With a literal block (
alertmanager.yaml: |), every finding points at its line and column inside the block, and# amlint: disable=...comments inside the block work as in a plain file. With base64 data or a quoted string the lines can't be mapped one to one, so findings point at thealertmanager.yaml:key. - The Secret's other keys are template files: the operator mounts them in
/etc/alertmanager/config/. Atemplates:entry in that directory is checked against them instead of against files on disk. A file name that isn't a key is an error, and a glob that matches none of them is a warning. A glob is only reported if the Secret has template files at all, since*.tmplwith no template files just loads nothing. Relative paths and other directories aren't checked: the operator runs Alertmanager on a copy of the config inconfig_out/, so they don't resolve next to the Secret. - A
ConfigMap(data, or base64binaryData) is read the same way. Its template files aren't checked, because where it is mounted depends on the chart.
kube-prometheus-stack values
alertmanager:
config:
route:
receiver: slack
receivers:
- name: slack
slack_configs:
- api_url: https://hooks.slack.com/services/T/B/X
Helm merges your values over the chart's own values.yaml, so the config Alertmanager gets
isn't only what your file says. amlint merges alertmanager.config over the default config
of kube-prometheus-stack 92.10.1 the way Helm does: maps key by key, lists replaced
whole, and null deletes a key. That catches a common mistake. The values above replace
receivers, so the chart's null receiver is gone, but the chart's Watchdog route to
null is still there:
note: values.yaml:2: alertmanager.config is merged over the kube-prometheus-stack 92.10.1 defaults, as Helm merges values: global, inhibit_rules, templates, route.routes come from the chart
✖ ERROR [undefined-receiver] · values.yaml:3:5 · route.routes[0]
Route references receiver 'null' which is not defined in receivers. Alerts matched here will be
dropped.
A finding in a part that only exists in the chart's defaults points at the nearest part your
file does have, here the route: the default routes are merged into.
alertmanager.templateFilesend up in the config Secret, sotemplates:entries in/etc/alertmanager/config/are checked against them, as for a Secret.alertmanager.stringConfigis only used withalertmanager.tplConfig: true, which is what the chart's Secret template does. Without it the chart rendersalertmanager.config, and amlint notes that the string is ignored. A stringalertmanager.configis read as a config.alertmanager.alertmanagerSpec.alertmanagerConfigMatcherStrategyis used for AlertmanagerConfig resources in the same stream.- Values given with
--setor in other values files aren't in the file. Lint the values Helm actually uses (helm get values <release> --all), or render the chart withhelm templateand lint its output, which holds the config Secret.
Mimir and Cortex
The format mimirtool alertmanager get prints and POST /api/v1/alerts takes:
template_files:
default_template: |
{{ define "email.subject" }}...{{ end }}
alertmanager_config: |
route:
receiver: team-email
...
templates: isn't checked against template_files. Mimir loads every template file of the
tenant, whatever templates: lists, and refuses entries that contain a path.
prometheus-operator AlertmanagerConfig
AlertmanagerConfig resources (monitoring.coreos.com/v1alpha1 and v1beta1) are
converted into Alertmanager's config and merged into the base config the way
prometheus-operator v0.94.1 does it (pkg/alertmanager/amcfg.go):
- Names. Receiver and time interval names become
<namespace>/<name>/<receiver>, soreceiver: pagerinteam-a/paymentsisteam-a/payments/pager. Findings,tree,routeandtestall use these names. - Namespace matcher. Each resource's route gets
namespace="<namespace>"added to its matchers, and so do both sides of each of its inhibit rules (unless a side already has it). So a resource only sees alerts from its own namespace. This followsalertmanagerConfigMatcherStrategy:OnNamespace(the default) adds it,Nonedoesn't, andOnNamespaceExceptForAlertmanagerNamespacedoesn't for resources in the Alertmanager's own namespace. The strategy comes from--matcher-strategy, else from anAlertmanagerresource in the input, else from Helm values in the input, else it isOnNamespace. - Order and
continue. The resources' routes go first under the root route, sorted bynamespace/name, and before the base config's own routes. Each hascontinue: true, so alerts also go on to the routes after it. - Secrets. Fields that reference a Secret (
apiURL,routingKey,apiKey,urlSecret,botToken, ...) count as set: the operator fills in the Secret's value. The value itself isn't read, so a Secret that doesn't exist isn't caught. - Fields. camelCase fields become Alertmanager's (
slackConfigs→slack_configs,groupWait→group_wait). A webhook'surlSecretis itsurl, a DiscordapiURLitswebhook_url. A zerogroupIntervalorrepeatIntervalis dropped, as the operator drops it, so the parent's value applies. - Versions. v1alpha1 spells the top-level time intervals
muteTimeIntervals, v1beta1timeIntervals, and amlint notes the other one, which the API server drops. A v1alpha1 matcher withoutmatchTypeusesregex: truefor=~. - Without a route. A resource without
spec.routeonly contributes its inhibit rules; the operator drops its receivers and time intervals, and amlint notes it. - Base config. A Secret, plain config, Helm values or Mimir config in the same stream is
the base. Without one, it is the operator's default config (a root route to a
nullreceiver), and findings about that default are dropped, since there is nothing in the input to fix. AnAlertmanagerresource whosespec.alertmanagerConfiguration.namenames one of the resources makes that resource the whole config, without a namespace matcher, as the operator does.
Some findings would only describe what the operator adds on purpose, so they are dropped:
inhibit-same-match, inhibit-no-equal and circular-inhibition when only the namespace
matcher causes them (it is on both sides of every rule of a resource), and
useless-continue on a resource's route when it happens to be the last one. Problems in the
rules themselves are still reported.
$ amlint tree alertmanagerconfigs.yaml
(root) → default
├── {namespace="team-a"} → team-a/payments/slack [continue]
│ └── {severity="critical"} → team-a/payments/pager
├── {service=~"search-.*", namespace="team-b"} → team-b/search/webhook [continue]
└── {alertname = "Watchdog"} → null
Routing tests
Test cases see the merged config, so their labels need a namespace, and receivers are
named the operator's way:
tests:
- name: payments critical alerts page
labels: {namespace: team-a, alertname: PaymentsDown, severity: critical}
expect: team-a/payments/pager
- name: another namespace never reaches team-a
labels: {namespace: team-c, alertname: PaymentsDown, severity: critical}
expect: default
- name: team-a's route has continue, so the base routes still see its alerts
labels: {namespace: team-a, alertname: Watchdog}
expect: [team-a/payments/slack, "null"]
Several files
The operator merges every selected resource, and amlint merges every resource in one input. To lint resources from several files together, put them in one stream:
kubectl kustomize deploy/monitoring | amlint check -
helm template monitoring ./chart | amlint check -
kubectl get alertmanagerconfigs -A -o yaml | amlint check -
for f in alertmanagerconfigs/*.yaml; do echo ---; cat "$f"; done | amlint check -
With stdin, findings point at lines of the stream. Passing the files one by one
(amlint check a.yaml b.yaml) lints each resource on its own, under the operator's default
config.
What isn't modeled
alertmanagerConfigSelectorandalertmanagerConfigNamespaceSelector: every resource in the input counts as selected.- The operator's validation, which skips a whole resource it rejects (say, a route to a receiver the resource doesn't define). amlint reports what is wrong instead.
- Alertmanager versions before 0.22: matchers are always written in the
matchers:syntax. - A resource without
metadata.namespaceis taken to be indefault, with a note.