Skip to content

Commit 9cacd7e

Browse files
nixos/prometheus/alertmanager-ntfy: init module (#402291)
2 parents 75a9d87 + e8e61ac commit 9cacd7e

File tree

13 files changed

+980
-712
lines changed

13 files changed

+980
-712
lines changed

nixos/doc/manual/release-notes/rl-2505.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@
166166

167167
- [OliveTin](https://www.olivetin.app/), gives safe and simple access to predefined shell commands from a web interface. Available as [services.olivetin](#opt-services.olivetin.enable).
168168

169+
- [alertmanager-ntfy](https://github.com/alexbakker/alertmanager-ntfy), forwards Prometheus Alertmanager notifications to ntfy.sh. Available as [services.prometheus.alertmanager-ntfy](#opt-services.prometheus.alertmanager-ntfy.enable).
170+
169171
- [Stash](https://github.com/stashapp/stash), An organizer for your adult videos/images, written in Go. Available as [services.stash](#opt-services.stash.enable).
170172

171173
- [vsmartcard-vpcd](https://frankmorgner.github.io/vsmartcard/virtualsmartcard/README.html), a virtual smart card driver. Available as [services.vsmartcard-vpcd](#opt-services.vsmartcard-vpcd.enable).

nixos/modules/module-list.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,7 @@
985985
./services/monitoring/pgscv.nix
986986
./services/monitoring/prometheus/alertmanager-gotify-bridge.nix
987987
./services/monitoring/prometheus/alertmanager-irc-relay.nix
988+
./services/monitoring/prometheus/alertmanager-ntfy.nix
988989
./services/monitoring/prometheus/alertmanager-webhook-logger.nix
989990
./services/monitoring/prometheus/alertmanager.nix
990991
./services/monitoring/prometheus/default.nix
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
8+
let
9+
cfg = config.services.prometheus.alertmanager-ntfy;
10+
11+
settingsFormat = pkgs.formats.yaml { };
12+
settingsFile = settingsFormat.generate "settings.yml" cfg.settings;
13+
14+
configsArg = lib.concatStringsSep "," (
15+
[ settingsFile ] ++ lib.imap0 (i: _: "%d/config-${toString i}.yml") cfg.extraConfigFiles
16+
);
17+
in
18+
19+
{
20+
meta.maintainers = with lib.maintainers; [ defelo ];
21+
22+
options.services.prometheus.alertmanager-ntfy = {
23+
enable = lib.mkEnableOption "alertmanager-ntfy";
24+
25+
package = lib.mkPackageOption pkgs "alertmanager-ntfy" { };
26+
27+
settings = lib.mkOption {
28+
description = ''
29+
Configuration of alertmanager-ntfy.
30+
See <https://github.com/alexbakker/alertmanager-ntfy> for more information.
31+
'';
32+
default = { };
33+
34+
type = lib.types.submodule {
35+
freeformType = settingsFormat.type;
36+
37+
options = {
38+
http.addr = lib.mkOption {
39+
type = lib.types.str;
40+
description = "The address to listen on.";
41+
default = "127.0.0.1:8000";
42+
example = ":8000";
43+
};
44+
45+
ntfy = {
46+
baseurl = lib.mkOption {
47+
type = lib.types.str;
48+
description = "The base URL of the ntfy.sh instance.";
49+
example = "https://ntfy.sh";
50+
};
51+
52+
notification = {
53+
topic = lib.mkOption {
54+
type = lib.types.str;
55+
description = ''
56+
The topic to which alerts should be published.
57+
Can either be a hardcoded string or a gval expression that evaluates to a string.
58+
'';
59+
example = "alertmanager";
60+
};
61+
62+
priority = lib.mkOption {
63+
type = lib.types.str;
64+
description = ''
65+
The ntfy.sh message priority (see <https://docs.ntfy.sh/publish/#message-priority> for more information).
66+
Can either be a hardcoded string or a gval expression that evaluates to a string.
67+
'';
68+
default = ''status == "firing" ? "high" : "default"'';
69+
};
70+
71+
tags = lib.mkOption {
72+
type = lib.types.listOf (
73+
lib.types.submodule {
74+
options = {
75+
tag = lib.mkOption {
76+
type = lib.types.str;
77+
description = ''
78+
The tag to add.
79+
See <https://docs.ntfy.sh/emojis> for a list of all supported emojis.
80+
'';
81+
example = "rotating_light";
82+
};
83+
84+
condition = lib.mkOption {
85+
type = lib.types.nullOr lib.types.str;
86+
description = ''
87+
The condition under which this tag should be added.
88+
Tags with no condition are always included.
89+
'';
90+
default = null;
91+
example = ''status == "firing"'';
92+
};
93+
};
94+
}
95+
);
96+
description = ''
97+
Tags to add to ntfy.sh messages.
98+
See <https://docs.ntfy.sh/publish/#tags-emojis> for more information.
99+
'';
100+
default = [
101+
{
102+
tag = "green_circle";
103+
condition = ''status == "resolved"'';
104+
}
105+
{
106+
tag = "red_circle";
107+
condition = ''status == "firing"'';
108+
}
109+
];
110+
};
111+
112+
templates = {
113+
title = lib.mkOption {
114+
type = lib.types.str;
115+
description = "The ntfy.sh message title template.";
116+
default = ''
117+
{{ if eq .Status "resolved" }}Resolved: {{ end }}{{ index .Annotations "summary" }}
118+
'';
119+
};
120+
121+
description = lib.mkOption {
122+
type = lib.types.str;
123+
description = "The ntfy.sh message description template.";
124+
default = ''
125+
{{ index .Annotations "description" }}
126+
'';
127+
};
128+
};
129+
};
130+
};
131+
};
132+
};
133+
};
134+
135+
extraConfigFiles = lib.mkOption {
136+
type = lib.types.listOf lib.types.path;
137+
default = [ ];
138+
example = [ "/run/secrets/alertmanager-ntfy.yml" ];
139+
description = ''
140+
Config files to merge into the settings defined in [](#opt-services.prometheus.alertmanager-ntfy.settings).
141+
This is useful to avoid putting secrets into the Nix store.
142+
See <https://github.com/alexbakker/alertmanager-ntfy> for more information.
143+
'';
144+
};
145+
};
146+
147+
config = lib.mkIf cfg.enable {
148+
systemd.services.alertmanager-ntfy = {
149+
wantedBy = [ "multi-user.target" ];
150+
151+
wants = [ "network-online.target" ];
152+
after = [ "network-online.target" ];
153+
154+
serviceConfig = {
155+
User = "alertmanager-ntfy";
156+
Group = "alertmanager-ntfy";
157+
DynamicUser = true;
158+
159+
LoadCredential = lib.imap0 (i: path: "config-${toString i}.yml:${path}") cfg.extraConfigFiles;
160+
161+
ExecStart = "${lib.getExe cfg.package} --configs ${configsArg}";
162+
163+
Restart = "always";
164+
RestartSec = 5;
165+
166+
# Hardening
167+
AmbientCapabilities = "";
168+
CapabilityBoundingSet = [ "" ];
169+
DevicePolicy = "closed";
170+
LockPersonality = true;
171+
MemoryDenyWriteExecute = true;
172+
NoNewPrivileges = true;
173+
PrivateDevices = true;
174+
PrivateTmp = true;
175+
PrivateUsers = true;
176+
ProcSubset = "pid";
177+
ProtectClock = true;
178+
ProtectControlGroups = true;
179+
ProtectHome = true;
180+
ProtectHostname = true;
181+
ProtectKernelLogs = true;
182+
ProtectKernelModules = true;
183+
ProtectKernelTunables = true;
184+
ProtectProc = "invisible";
185+
ProtectSystem = "strict";
186+
RemoveIPC = true;
187+
RestrictAddressFamilies = [ "AF_INET AF_INET6" ];
188+
RestrictNamespaces = true;
189+
RestrictRealtime = true;
190+
RestrictSUIDSGID = true;
191+
SystemCallArchitectures = "native";
192+
SystemCallFilter = [
193+
"@system-service"
194+
"~@privileged"
195+
"~@resources"
196+
];
197+
UMask = "0077";
198+
};
199+
};
200+
};
201+
}

nixos/tests/all-tests.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1117,7 +1117,7 @@ in
11171117
private-gpt = handleTest ./private-gpt.nix { };
11181118
privatebin = runTest ./privatebin.nix;
11191119
privoxy = handleTest ./privoxy.nix { };
1120-
prometheus = handleTest ./prometheus { };
1120+
prometheus = import ./prometheus { inherit runTest; };
11211121
prometheus-exporters = handleTest ./prometheus-exporters.nix { };
11221122
prosody = handleTest ./xmpp/prosody.nix { };
11231123
prosody-mysql = handleTest ./xmpp/prosody-mysql.nix { };
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{ lib, ... }:
2+
3+
let
4+
ports = {
5+
alertmanager-ntfy = 8000;
6+
ntfy-sh = 8001;
7+
alertmanager = 8002;
8+
};
9+
in
10+
11+
{
12+
name = "alertmanager-ntfy";
13+
meta.maintainers = with lib.maintainers; [ defelo ];
14+
15+
nodes.machine = {
16+
services.prometheus.alertmanager = {
17+
enable = true;
18+
listenAddress = "127.0.0.1";
19+
port = ports.alertmanager;
20+
21+
configuration = {
22+
route = {
23+
receiver = "test";
24+
group_by = [ "..." ];
25+
group_wait = "0s";
26+
group_interval = "1s";
27+
repeat_interval = "2h";
28+
};
29+
30+
receivers = [
31+
{
32+
name = "test";
33+
webhook_configs = [ { url = "http://127.0.0.1:${toString ports.alertmanager-ntfy}/hook"; } ];
34+
}
35+
];
36+
};
37+
};
38+
39+
services.prometheus.alertmanager-ntfy = {
40+
enable = true;
41+
settings = {
42+
http.addr = "127.0.0.1:${toString ports.alertmanager-ntfy}";
43+
ntfy = {
44+
baseurl = "http://127.0.0.1:${toString ports.ntfy-sh}";
45+
notification.topic = "alertmanager";
46+
};
47+
};
48+
};
49+
50+
services.ntfy-sh = {
51+
enable = true;
52+
settings = {
53+
listen-http = "127.0.0.1:${toString ports.ntfy-sh}";
54+
base-url = "http://127.0.0.1:${toString ports.ntfy-sh}";
55+
};
56+
};
57+
};
58+
59+
interactive.nodes.machine = {
60+
services.prometheus.alertmanager.listenAddress = lib.mkForce "0.0.0.0";
61+
services.prometheus.alertmanager-ntfy.settings.http.addr =
62+
lib.mkForce "0.0.0.0:${toString ports.alertmanager-ntfy}";
63+
services.ntfy-sh.settings.listen-http = lib.mkForce "0.0.0.0:${toString ports.ntfy-sh}";
64+
networking.firewall.enable = false;
65+
virtualisation.forwardPorts = lib.mapAttrsToList (_: port: {
66+
from = "host";
67+
host = { inherit port; };
68+
guest = { inherit port; };
69+
}) ports;
70+
};
71+
72+
testScript = ''
73+
import json
74+
import time
75+
76+
machine.wait_for_unit("alertmanager.service")
77+
machine.wait_for_unit("alertmanager-ntfy.service")
78+
machine.wait_for_unit("ntfy-sh.service")
79+
machine.wait_for_open_port(${toString ports.alertmanager})
80+
machine.wait_for_open_port(${toString ports.alertmanager-ntfy})
81+
machine.wait_for_open_port(${toString ports.ntfy-sh})
82+
83+
machine.succeed("""curl 127.0.0.1:${toString ports.alertmanager}/api/v2/alerts \
84+
-X POST -H 'Content-Type: application/json' \
85+
-d '[{ \
86+
"labels": {"alertname": "test"},
87+
"annotations": {"summary": "alert summary", "description": "alert description"} \
88+
}]'""")
89+
90+
while not (resp := machine.succeed("curl '127.0.0.1:${toString ports.ntfy-sh}/alertmanager/json?poll=1'")):
91+
time.sleep(1)
92+
93+
msg = json.loads(resp)
94+
assert msg["title"] == "alert summary"
95+
assert msg["message"] == "alert description"
96+
assert msg["priority"] == 4
97+
assert "red_circle" in msg["tags"]
98+
'';
99+
}

0 commit comments

Comments
 (0)