lol

nixos/webhook: init

authored by

Lucas Franceschino and committed by
Naïm Favier
e8e932bc fbc4d64b

+293
+7
nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
··· 39 39 </listitem> 40 40 <listitem> 41 41 <para> 42 + <link xlink:href="https://github.com/adnanh/webhook">webhook</link>, 43 + a lightweight webhook server. Available as 44 + <link linkend="opt-services.webhook.enable">services.webhook</link>. 45 + </para> 46 + </listitem> 47 + <listitem> 48 + <para> 42 49 <link xlink:href="https://github.com/alexivkin/CUPS-PDF-to-PDF">cups-pdf-to-pdf</link>, 43 50 a pdf-generating cups backend based on 44 51 <link xlink:href="https://www.cups-pdf.de/">cups-pdf</link>.
+2
nixos/doc/manual/release-notes/rl-2305.section.md
··· 18 18 19 19 - [blesh](https://github.com/akinomyoga/ble.sh), a line editor written in pure bash. Available as [programs.bash.blesh](#opt-programs.bash.blesh.enable). 20 20 21 + - [webhook](https://github.com/adnanh/webhook), a lightweight webhook server. Available as [services.webhook](#opt-services.webhook.enable). 22 + 21 23 - [cups-pdf-to-pdf](https://github.com/alexivkin/CUPS-PDF-to-PDF), a pdf-generating cups backend based on [cups-pdf](https://www.cups-pdf.de/). Available as [services.printing.cups-pdf](#opt-services.printing.cups-pdf.enable). 22 24 23 25 - [fzf](https://github.com/junegunn/fzf), a command line fuzzyfinder. Available as [programs.fzf](#opt-programs.fzf.fuzzyCompletion).
+1
nixos/modules/module-list.nix
··· 1012 1012 ./services/networking/wasabibackend.nix 1013 1013 ./services/networking/websockify.nix 1014 1014 ./services/networking/wg-netmanager.nix 1015 + ./services/networking/webhook.nix 1015 1016 ./services/networking/wg-quick.nix 1016 1017 ./services/networking/wireguard.nix 1017 1018 ./services/networking/wpa_supplicant.nix
+214
nixos/modules/services/networking/webhook.nix
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + 5 + let 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 + 31 + in { 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 = mkPackageOption 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 + }
+1
nixos/tests/all-tests.nix
··· 709 709 vsftpd = handleTest ./vsftpd.nix {}; 710 710 warzone2100 = handleTest ./warzone2100.nix {}; 711 711 wasabibackend = handleTest ./wasabibackend.nix {}; 712 + webhook = runTest ./webhook.nix; 712 713 wiki-js = handleTest ./wiki-js.nix {}; 713 714 wine = handleTest ./wine.nix {}; 714 715 wireguard = handleTest ./wireguard {};
+65
nixos/tests/webhook.nix
··· 1 + { pkgs, ... }: 2 + let 3 + forwardedPort = 19000; 4 + internalPort = 9000; 5 + in 6 + { 7 + name = "webhook"; 8 + 9 + nodes = { 10 + webhookMachine = { pkgs, ... }: { 11 + virtualisation.forwardPorts = [{ 12 + host.port = forwardedPort; 13 + guest.port = internalPort; 14 + }]; 15 + services.webhook = { 16 + enable = true; 17 + port = internalPort; 18 + openFirewall = true; 19 + hooks = { 20 + echo = { 21 + execute-command = "echo"; 22 + response-message = "Webhook is reachable!"; 23 + }; 24 + }; 25 + hooksTemplated = { 26 + echoTemplate = '' 27 + { 28 + "id": "echo-template", 29 + "execute-command": "echo", 30 + "response-message": "{{ getenv "WEBHOOK_MESSAGE" }}" 31 + } 32 + ''; 33 + }; 34 + environment.WEBHOOK_MESSAGE = "Templates are working!"; 35 + }; 36 + }; 37 + }; 38 + 39 + extraPythonPackages = p: [ 40 + p.requests 41 + p.types-requests 42 + ]; 43 + 44 + testScript = { nodes, ... }: '' 45 + import requests 46 + webhookMachine.wait_for_unit("webhook") 47 + webhookMachine.wait_for_open_port(${toString internalPort}) 48 + 49 + with subtest("Check that webhooks can be called externally"): 50 + response = requests.get("http://localhost:${toString forwardedPort}/hooks/echo") 51 + print(f"Response code: {response.status_code}") 52 + print("Response: %r" % response.content) 53 + 54 + assert response.status_code == 200 55 + assert response.content == b"Webhook is reachable!" 56 + 57 + with subtest("Check that templated webhooks can be called externally"): 58 + response = requests.get("http://localhost:${toString forwardedPort}/hooks/echo-template") 59 + print(f"Response code: {response.status_code}") 60 + print("Response: %r" % response.content) 61 + 62 + assert response.status_code == 200 63 + assert response.content == b"Templates are working!" 64 + ''; 65 + }
+3
pkgs/servers/http/webhook/default.nix
··· 1 1 { lib 2 2 , buildGoModule 3 3 , fetchFromGitHub 4 + , nixosTests 4 5 }: 5 6 6 7 buildGoModule rec { ··· 19 20 subPackages = [ "." ]; 20 21 21 22 doCheck = false; 23 + 24 + passthru.tests = { inherit (nixosTests) webhook; }; 22 25 23 26 meta = with lib; { 24 27 description = "Incoming webhook server that executes shell commands";