1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.webhook;
12 defaultUser = "webhook";
13
14 hookFormat = pkgs.formats.json { };
15
16 hookType = types.submodule (
17 { name, ... }:
18 {
19 freeformType = hookFormat.type;
20 options = {
21 id = mkOption {
22 type = types.str;
23 default = name;
24 description = ''
25 The ID of your hook. This value is used to create the HTTP endpoint (`protocol://yourserver:port/prefix/''${id}`).
26 '';
27 };
28 execute-command = mkOption {
29 type = types.str;
30 description = "The command that should be executed when the hook is triggered.";
31 };
32 };
33 }
34 );
35
36 hookFiles =
37 mapAttrsToList (name: hook: hookFormat.generate "webhook-${name}.json" [ hook ]) cfg.hooks
38 ++ mapAttrsToList (
39 name: hook: pkgs.writeText "webhook-${name}.json.tmpl" "[${hook}]"
40 ) cfg.hooksTemplated;
41
42in
43{
44 options = {
45 services.webhook = {
46 enable = mkEnableOption ''
47 [Webhook](https://github.com/adnanh/webhook), a server written in Go that allows you to create HTTP endpoints (hooks),
48 which execute configured commands for any person or service that knows the URL
49 '';
50
51 package = mkPackageOption pkgs "webhook" { };
52 user = mkOption {
53 type = types.str;
54 default = defaultUser;
55 description = ''
56 Webhook will be run under this user.
57
58 If set, you must create this user yourself!
59 '';
60 };
61 group = mkOption {
62 type = types.str;
63 default = defaultUser;
64 description = ''
65 Webhook will be run under this group.
66
67 If set, you must create this group yourself!
68 '';
69 };
70 ip = mkOption {
71 type = types.str;
72 default = "0.0.0.0";
73 description = ''
74 The IP webhook should serve hooks on.
75
76 The default means it can be reached on any interface if `openFirewall = true`.
77 '';
78 };
79 port = mkOption {
80 type = types.port;
81 default = 9000;
82 description = "The port webhook should be reachable from.";
83 };
84 openFirewall = mkOption {
85 type = types.bool;
86 default = false;
87 description = ''
88 Open the configured port in the firewall for external ingress traffic.
89 Preferably the Webhook server is instead put behind a reverse proxy.
90 '';
91 };
92 enableTemplates = mkOption {
93 type = types.bool;
94 default = cfg.hooksTemplated != { };
95 defaultText = literalExpression "hooksTemplated != {}";
96 description = ''
97 Enable the generated hooks file to be parsed as a Go template.
98 See [the documentation](https://github.com/adnanh/webhook/blob/master/docs/Templates.md) for more information.
99 '';
100 };
101 urlPrefix = mkOption {
102 type = types.str;
103 default = "hooks";
104 description = ''
105 The URL path prefix to use for served hooks (`protocol://yourserver:port/''${prefix}/hook-id`).
106 '';
107 };
108 hooks = mkOption {
109 type = types.attrsOf hookType;
110 default = { };
111 example = {
112 echo = {
113 execute-command = "echo";
114 response-message = "Webhook is reachable!";
115 };
116 redeploy-webhook = {
117 execute-command = "/var/scripts/redeploy.sh";
118 command-working-directory = "/var/webhook";
119 };
120 };
121 description = ''
122 The actual configuration of which hooks will be served.
123
124 Read more on the [project homepage] and on the [hook definition] page.
125 At least one hook needs to be configured.
126
127 [hook definition]: https://github.com/adnanh/webhook/blob/master/docs/Hook-Definition.md
128 [project homepage]: https://github.com/adnanh/webhook#configuration
129 '';
130 };
131 hooksTemplated = mkOption {
132 type = types.attrsOf types.str;
133 default = { };
134 example = {
135 echo-template = ''
136 {
137 "id": "echo-template",
138 "execute-command": "echo",
139 "response-message": "{{ getenv "MESSAGE" }}"
140 }
141 '';
142 };
143 description = ''
144 Same as {option}`hooks`, but these hooks are specified as literal strings instead of Nix values,
145 and hence can include [template syntax](https://github.com/adnanh/webhook/blob/master/docs/Templates.md)
146 which might not be representable as JSON.
147
148 Template syntax requires the {option}`enableTemplates` option to be set to `true`, which is
149 done by default if this option is set.
150 '';
151 };
152 verbose = mkOption {
153 type = types.bool;
154 default = true;
155 description = "Whether to show verbose output.";
156 };
157 extraArgs = mkOption {
158 type = types.listOf types.str;
159 default = [ ];
160 example = [ "-secure" ];
161 description = ''
162 These are arguments passed to the webhook command in the systemd service.
163 You can find the available arguments and options in the [documentation][parameters].
164
165 [parameters]: https://github.com/adnanh/webhook/blob/master/docs/Webhook-Parameters.md
166 '';
167 };
168 environment = mkOption {
169 type = types.attrsOf types.str;
170 default = { };
171 description = "Extra environment variables passed to webhook.";
172 };
173 };
174 };
175
176 config = mkIf cfg.enable {
177 assertions =
178 let
179 overlappingHooks = builtins.intersectAttrs cfg.hooks cfg.hooksTemplated;
180 in
181 [
182 {
183 assertion = hookFiles != [ ];
184 message = "At least one hook needs to be configured for webhook to run.";
185 }
186 {
187 assertion = overlappingHooks == { };
188 message = "`services.webhook.hooks` and `services.webhook.hooksTemplated` have overlapping attribute(s): ${concatStringsSep ", " (builtins.attrNames overlappingHooks)}";
189 }
190 ];
191
192 users.users = mkIf (cfg.user == defaultUser) {
193 ${defaultUser} = {
194 isSystemUser = true;
195 group = cfg.group;
196 description = "Webhook daemon user";
197 };
198 };
199
200 users.groups = mkIf (cfg.user == defaultUser && cfg.group == defaultUser) {
201 ${defaultUser} = { };
202 };
203
204 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
205
206 systemd.services.webhook = {
207 description = "Webhook service";
208 after = [ "network.target" ];
209 wantedBy = [ "multi-user.target" ];
210 environment = config.networking.proxy.envVars // cfg.environment;
211 script =
212 let
213 args =
214 [
215 "-ip"
216 cfg.ip
217 "-port"
218 (toString cfg.port)
219 "-urlprefix"
220 cfg.urlPrefix
221 ]
222 ++ concatMap (hook: [
223 "-hooks"
224 hook
225 ]) hookFiles
226 ++ optional cfg.enableTemplates "-template"
227 ++ optional cfg.verbose "-verbose"
228 ++ cfg.extraArgs;
229 in
230 ''
231 ${cfg.package}/bin/webhook ${escapeShellArgs args}
232 '';
233 serviceConfig = {
234 Restart = "on-failure";
235 User = cfg.user;
236 Group = cfg.group;
237 };
238 };
239 };
240}