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