nixos/webhook: init

Changed files
+293
nixos
doc
manual
from_md
release-notes
release-notes
modules
services
networking
tests
pkgs
servers
http
webhook
+7
nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
···
</listitem>
<listitem>
<para>
+
<link xlink:href="https://github.com/adnanh/webhook">webhook</link>,
+
a lightweight webhook server. Available as
+
<link linkend="opt-services.webhook.enable">services.webhook</link>.
+
</para>
+
</listitem>
+
<listitem>
+
<para>
<link xlink:href="https://github.com/alexivkin/CUPS-PDF-to-PDF">cups-pdf-to-pdf</link>,
a pdf-generating cups backend based on
<link xlink:href="https://www.cups-pdf.de/">cups-pdf</link>.
+2
nixos/doc/manual/release-notes/rl-2305.section.md
···
- [blesh](https://github.com/akinomyoga/ble.sh), a line editor written in pure bash. Available as [programs.bash.blesh](#opt-programs.bash.blesh.enable).
+
- [webhook](https://github.com/adnanh/webhook), a lightweight webhook server. Available as [services.webhook](#opt-services.webhook.enable).
+
- [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).
- [fzf](https://github.com/junegunn/fzf), a command line fuzzyfinder. Available as [programs.fzf](#opt-programs.fzf.fuzzyCompletion).
+1
nixos/modules/module-list.nix
···
./services/networking/wasabibackend.nix
./services/networking/websockify.nix
./services/networking/wg-netmanager.nix
+
./services/networking/webhook.nix
./services/networking/wg-quick.nix
./services/networking/wireguard.nix
./services/networking/wpa_supplicant.nix
+214
nixos/modules/services/networking/webhook.nix
···
+
{ config, lib, pkgs, ... }:
+
+
with lib;
+
+
let
+
cfg = config.services.webhook;
+
defaultUser = "webhook";
+
+
hookFormat = pkgs.formats.json {};
+
+
hookType = types.submodule ({ name, ... }: {
+
freeformType = hookFormat.type;
+
options = {
+
id = mkOption {
+
type = types.str;
+
default = name;
+
description = mdDoc ''
+
The ID of your hook. This value is used to create the HTTP endpoint (`protocol://yourserver:port/prefix/''${id}`).
+
'';
+
};
+
execute-command = mkOption {
+
type = types.str;
+
description = mdDoc "The command that should be executed when the hook is triggered.";
+
};
+
};
+
});
+
+
hookFiles = mapAttrsToList (name: hook: hookFormat.generate "webhook-${name}.json" [ hook ]) cfg.hooks
+
++ mapAttrsToList (name: hook: pkgs.writeText "webhook-${name}.json.tmpl" "[${hook}]") cfg.hooksTemplated;
+
+
in {
+
options = {
+
services.webhook = {
+
enable = mkEnableOption (mdDoc ''
+
[Webhook](https://github.com/adnanh/webhook), a server written in Go that allows you to create HTTP endpoints (hooks),
+
which execute configured commands for any person or service that knows the URL
+
'');
+
+
package = mkPackageOption pkgs "webhook" {};
+
user = mkOption {
+
type = types.str;
+
default = defaultUser;
+
description = mdDoc ''
+
Webhook will be run under this user.
+
+
If set, you must create this user yourself!
+
'';
+
};
+
group = mkOption {
+
type = types.str;
+
default = defaultUser;
+
description = mdDoc ''
+
Webhook will be run under this group.
+
+
If set, you must create this group yourself!
+
'';
+
};
+
ip = mkOption {
+
type = types.str;
+
default = "0.0.0.0";
+
description = mdDoc ''
+
The IP webhook should serve hooks on.
+
+
The default means it can be reached on any interface if `openFirewall = true`.
+
'';
+
};
+
port = mkOption {
+
type = types.port;
+
default = 9000;
+
description = mdDoc "The port webhook should be reachable from.";
+
};
+
openFirewall = mkOption {
+
type = types.bool;
+
default = false;
+
description = lib.mdDoc ''
+
Open the configured port in the firewall for external ingress traffic.
+
Preferably the Webhook server is instead put behind a reverse proxy.
+
'';
+
};
+
enableTemplates = mkOption {
+
type = types.bool;
+
default = cfg.hooksTemplated != {};
+
defaultText = literalExpression "hooksTemplated != {}";
+
description = mdDoc ''
+
Enable the generated hooks file to be parsed as a Go template.
+
See [the documentation](https://github.com/adnanh/webhook/blob/master/docs/Templates.md) for more information.
+
'';
+
};
+
urlPrefix = mkOption {
+
type = types.str;
+
default = "hooks";
+
description = mdDoc ''
+
The URL path prefix to use for served hooks (`protocol://yourserver:port/''${prefix}/hook-id`).
+
'';
+
};
+
hooks = mkOption {
+
type = types.attrsOf hookType;
+
default = {};
+
example = {
+
echo = {
+
execute-command = "echo";
+
response-message = "Webhook is reachable!";
+
};
+
redeploy-webhook = {
+
execute-command = "/var/scripts/redeploy.sh";
+
command-working-directory = "/var/webhook";
+
};
+
};
+
description = mdDoc ''
+
The actual configuration of which hooks will be served.
+
+
Read more on the [project homepage] and on the [hook definition] page.
+
At least one hook needs to be configured.
+
+
[hook definition]: https://github.com/adnanh/webhook/blob/master/docs/Hook-Definition.md
+
[project homepage]: https://github.com/adnanh/webhook#configuration
+
'';
+
};
+
hooksTemplated = mkOption {
+
type = types.attrsOf types.str;
+
default = {};
+
example = {
+
echo-template = ''
+
{
+
"id": "echo-template",
+
"execute-command": "echo",
+
"response-message": "{{ getenv "MESSAGE" }}"
+
}
+
'';
+
};
+
description = mdDoc ''
+
Same as {option}`hooks`, but these hooks are specified as literal strings instead of Nix values,
+
and hence can include [template syntax](https://github.com/adnanh/webhook/blob/master/docs/Templates.md)
+
which might not be representable as JSON.
+
+
Template syntax requires the {option}`enableTemplates` option to be set to `true`, which is
+
done by default if this option is set.
+
'';
+
};
+
verbose = mkOption {
+
type = types.bool;
+
default = true;
+
description = mdDoc "Whether to show verbose output.";
+
};
+
extraArgs = mkOption {
+
type = types.listOf types.str;
+
default = [];
+
example = [ "-secure" ];
+
description = mdDoc ''
+
These are arguments passed to the webhook command in the systemd service.
+
You can find the available arguments and options in the [documentation][parameters].
+
+
[parameters]: https://github.com/adnanh/webhook/blob/master/docs/Webhook-Parameters.md
+
'';
+
};
+
environment = mkOption {
+
type = types.attrsOf types.str;
+
default = {};
+
description = mdDoc "Extra environment variables passed to webhook.";
+
};
+
};
+
};
+
+
config = mkIf cfg.enable {
+
assertions = let
+
overlappingHooks = builtins.intersectAttrs cfg.hooks cfg.hooksTemplated;
+
in [
+
{
+
assertion = hookFiles != [];
+
message = "At least one hook needs to be configured for webhook to run.";
+
}
+
{
+
assertion = overlappingHooks == {};
+
message = "`services.webhook.hooks` and `services.webhook.hooksTemplated` have overlapping attribute(s): ${concatStringsSep ", " (builtins.attrNames overlappingHooks)}";
+
}
+
];
+
+
users.users = mkIf (cfg.user == defaultUser) {
+
${defaultUser} =
+
{
+
isSystemUser = true;
+
group = cfg.group;
+
description = "Webhook daemon user";
+
};
+
};
+
+
users.groups = mkIf (cfg.user == defaultUser && cfg.group == defaultUser) {
+
${defaultUser} = {};
+
};
+
+
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+
systemd.services.webhook = {
+
description = "Webhook service";
+
after = [ "network.target" ];
+
wantedBy = [ "multi-user.target" ];
+
environment = config.networking.proxy.envVars // cfg.environment;
+
script = let
+
args = [ "-ip" cfg.ip "-port" (toString cfg.port) "-urlprefix" cfg.urlPrefix ]
+
++ concatMap (hook: [ "-hooks" hook ]) hookFiles
+
++ optional cfg.enableTemplates "-template"
+
++ optional cfg.verbose "-verbose"
+
++ cfg.extraArgs;
+
in ''
+
${cfg.package}/bin/webhook ${escapeShellArgs args}
+
'';
+
serviceConfig = {
+
Restart = "on-failure";
+
User = cfg.user;
+
Group = cfg.group;
+
};
+
};
+
};
+
}
+1
nixos/tests/all-tests.nix
···
vsftpd = handleTest ./vsftpd.nix {};
warzone2100 = handleTest ./warzone2100.nix {};
wasabibackend = handleTest ./wasabibackend.nix {};
+
webhook = runTest ./webhook.nix;
wiki-js = handleTest ./wiki-js.nix {};
wine = handleTest ./wine.nix {};
wireguard = handleTest ./wireguard {};
+65
nixos/tests/webhook.nix
···
+
{ pkgs, ... }:
+
let
+
forwardedPort = 19000;
+
internalPort = 9000;
+
in
+
{
+
name = "webhook";
+
+
nodes = {
+
webhookMachine = { pkgs, ... }: {
+
virtualisation.forwardPorts = [{
+
host.port = forwardedPort;
+
guest.port = internalPort;
+
}];
+
services.webhook = {
+
enable = true;
+
port = internalPort;
+
openFirewall = true;
+
hooks = {
+
echo = {
+
execute-command = "echo";
+
response-message = "Webhook is reachable!";
+
};
+
};
+
hooksTemplated = {
+
echoTemplate = ''
+
{
+
"id": "echo-template",
+
"execute-command": "echo",
+
"response-message": "{{ getenv "WEBHOOK_MESSAGE" }}"
+
}
+
'';
+
};
+
environment.WEBHOOK_MESSAGE = "Templates are working!";
+
};
+
};
+
};
+
+
extraPythonPackages = p: [
+
p.requests
+
p.types-requests
+
];
+
+
testScript = { nodes, ... }: ''
+
import requests
+
webhookMachine.wait_for_unit("webhook")
+
webhookMachine.wait_for_open_port(${toString internalPort})
+
+
with subtest("Check that webhooks can be called externally"):
+
response = requests.get("http://localhost:${toString forwardedPort}/hooks/echo")
+
print(f"Response code: {response.status_code}")
+
print("Response: %r" % response.content)
+
+
assert response.status_code == 200
+
assert response.content == b"Webhook is reachable!"
+
+
with subtest("Check that templated webhooks can be called externally"):
+
response = requests.get("http://localhost:${toString forwardedPort}/hooks/echo-template")
+
print(f"Response code: {response.status_code}")
+
print("Response: %r" % response.content)
+
+
assert response.status_code == 200
+
assert response.content == b"Templates are working!"
+
'';
+
}
+3
pkgs/servers/http/webhook/default.nix
···
{ lib
, buildGoModule
, fetchFromGitHub
+
, nixosTests
}:
buildGoModule rec {
···
subPackages = [ "." ];
doCheck = false;
+
+
passthru.tests = { inherit (nixosTests) webhook; };
meta = with lib; {
description = "Incoming webhook server that executes shell commands";