diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index b23517fcce16..c77cca562a6c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1834,6 +1834,7 @@ ./services/web-servers/unit/default.nix ./services/web-servers/uwsgi.nix ./services/web-servers/varnish/default.nix + ./services/web-servers/vinyl-cache/default.nix ./services/x11/clight.nix ./services/x11/colord.nix ./services/x11/desktop-managers/default.nix diff --git a/nixos/modules/services/web-servers/vinyl-cache/default.nix b/nixos/modules/services/web-servers/vinyl-cache/default.nix new file mode 100644 index 000000000000..ba498537edc8 --- /dev/null +++ b/nixos/modules/services/web-servers/vinyl-cache/default.nix @@ -0,0 +1,240 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + types + mkOption + hasPrefix + concatMapStringsSep + optionalString + concatMap + ; + + cfg = config.services.vinyl-cache; + + # Vinyl Cache has very strong opinions and very complicated code around handling + # the stateDir. After a lot of back and forth, we decided that we a) + # do not want a configurable option here, as most of the handling depends + # on the version and the compile time options. Putting everything into + # /var/run (RAM backed) is absolutely recommended by Vinyl Cache anyways. + # We do need to pay attention to the version-dependend variations, though! + stateDir = "/var/run/vinyld"; + + # from --help: + # -a [=]address[:port][,proto] # HTTP listen address and port + # [,user=][,group=] # Can be specified multiple times. + # [,mode=] # default: ":80,HTTP" + # # Proto can be "PROXY" or "HTTP" (default) + # # user, group and mode set permissions for + # # a Unix domain socket. + commandLineAddresses = ( + concatMapStringsSep " " ( + a: + "-a " + + optionalString (!isNull a.name) "${a.name}=" + + a.address + + optionalString (!isNull a.port) ":${toString a.port}" + + optionalString (!isNull a.proto) ",${a.proto}" + + optionalString (!isNull a.user) ",user=${a.user}" + + optionalString (!isNull a.group) ",group=${a.group}" + + optionalString (!isNull a.mode) ",mode=${a.mode}" + ) cfg.listen + ); + + addressSubmodule = types.submodule { + options = { + name = mkOption { + description = "Name is referenced in logs. If name is not specified, 'a0', 'a1', etc. is used."; + default = null; + type = with types; nullOr str; + }; + address = mkOption { + description = '' + If given an IP address, it can be a host name ("localhost"), an IPv4 dotted-quad + ("127.0.0.1") or an IPv6 address enclosed in square brackets ("[::1]"). + + (VCL4.1 and higher) If given an absolute Path ("/path/to/listen.sock") or "@" + followed by the name of an abstract socket ("@myvinyld") accept connections + on a Unix domain socket. + + The user, group and mode sub-arguments may be used to specify the permissions + of the socket file. These sub-arguments do not apply to abstract sockets. + ''; + type = types.str; + }; + port = mkOption { + description = "The port to use for IP sockets. If port is not specified, port 80 (http) is used."; + default = null; + type = with types; nullOr port; + }; + proto = mkOption { + description = "PROTO can be 'HTTP' (the default) or 'PROXY'. Both version 1 and 2 of the proxy protocol can be used."; + type = types.enum [ + "HTTP" + "PROXY" + ]; + default = "HTTP"; + }; + user = mkOption { + description = "User name who owns the socket file."; + default = null; + type = with lib.types; nullOr str; + }; + group = mkOption { + description = "Group name who owns the socket file."; + default = null; + type = with lib.types; nullOr str; + }; + mode = mkOption { + description = "Permission of the socket file (3-digit octal value)."; + default = null; + type = with types; nullOr str; + }; + }; + }; + checkedAddressModule = types.addCheck addressSubmodule ( + m: + ( + if ((hasPrefix "@" m.address) || (hasPrefix "/" m.address)) then + # this is a unix socket + (m.port != null) + else + # this is not a path-based unix socket + if !(hasPrefix "/" m.address) && (m.group != null) || (m.user != null) || (m.mode != null) then + false + else + true + ) + ); + commandLine = + "-f ${pkgs.writeText "default.vcl" cfg.config}" + + + lib.optionalString (cfg.extraModules != [ ]) + " -p vmod_path='${ + lib.makeSearchPathOutput "lib" "lib/vinyl/vmods" ([ cfg.package ] ++ cfg.extraModules) + }' -r vmod_path"; +in +{ + meta.maintainers = [ + lib.maintainers.leona + lib.maintainers.osnyx + ]; + options = { + services.vinyl-cache = { + enable = lib.mkEnableOption "Vinyl Cache"; + + enableConfigCheck = lib.mkEnableOption "checking the config during build time" // { + default = true; + }; + + package = lib.mkPackageOption pkgs "vinyl-cache" { }; + + listen = lib.mkOption { + description = "Accept for client requests on the specified listen addresses."; + type = lib.types.listOf checkedAddressModule; + defaultText = lib.literalExpression ''[ { address="*"; port=6081; } ]''; + default = [ + { + address = "*"; + port = 6081; + } + ]; + }; + + config = lib.mkOption { + type = lib.types.lines; + description = '' + Verbatim default.vcl configuration. + ''; + }; + + extraModules = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = '' + Vinyl Cache modules (except 'std'). + ''; + }; + + extraCommandLine = lib.mkOption { + type = lib.types.str; + default = ""; + example = "-s malloc,256M"; + description = '' + Command line switches for vinyld (run 'vinyld -?' to get list of options) + ''; + }; + + enableFileLogging = lib.mkEnableOption "file based logging"; + }; + + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.enable { + systemd.services.vinyl-cache = { + description = "Vinyl Cache"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/vinyld ${commandLineAddresses} -n ${stateDir} -F ${cfg.extraCommandLine} ${commandLine}"; + Restart = "always"; + RestartSec = "5s"; + User = "vinyl-cache"; + Group = "vinyl-cache"; + DynamicUser = true; + RuntimeDirectory = lib.removePrefix "/var/run/" stateDir; + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + NoNewPrivileges = true; + LimitNOFILE = 131072; + }; + }; + + environment.systemPackages = [ cfg.package ]; + + # check .vcl syntax at compile time (e.g. before nixops deployment) + system.checks = lib.mkIf cfg.enableConfigCheck [ + (pkgs.runCommand "check-vinyl-cache-syntax" { } '' + ${cfg.package}/bin/vinyld -C ${commandLine} 2> $out || (cat $out; exit 1) + '') + ]; + + (lib.mkIf (cfg.enable && cfg.enableFileLogging) { + systemd.services = { + vinylncsa = { + after = [ "vinyl-cache.service" ]; + requires = [ "vinyl-cache.service" ]; + description = "Vinyl Cache logging daemon"; + wantedBy = [ "multi-user.target" ]; + # We want to reopen logs with HUP. vinylncsa must run in daemon mode for that. + serviceConfig = { + Type = "forking"; + Restart = "always"; + RuntimeDirectory = "vinylncsa"; + LogsDirectory = "vinyl-cache"; + PIDFile = "/run/vinylncsa/vinylncsa.pid"; + User = "vinyl-cache"; + Group = "vinyl-cache"; + ExecStart = "${cfg.package}/bin/vinylncsa -D -a -w /var/log/vinyl-cache/vinyl-cache.log -P /run/vinylncsa/vinylncsa.pid"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + }; + + services.logrotate.settings.vinyl-cache = lib.mapAttrs (_: lib.mkDefault) { + files = [ "/var/log/vinyl-cache/*.log" ]; + frequency = "daily"; + rotate = 14; + compress = true; + delaycompress = true; + postrotate = "systemctl reload vinylncsa"; + }; + }) + ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 929e8362e8f3..68ece27faa63 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1743,6 +1743,10 @@ in victoriametrics = import ./victoriametrics { inherit runTest; }; victoriatraces = import ./victoriatraces { inherit runTest; }; vikunja = runTest ./vikunja.nix; + vinyl-cache_9 = runTest { + imports = [ ./vinyl-cache.nix ]; + _module.args.package = pkgs.vinyl-cache_9; + }; virtualbox = handleTestOn [ "x86_64-linux" ] ./virtualbox.nix { }; vm-variant = handleTest ./vm-variant.nix { }; vscode-remote-ssh = handleTestOn [ "x86_64-linux" ] ./vscode-remote-ssh.nix { }; diff --git a/nixos/tests/vinyl-cache.nix b/nixos/tests/vinyl-cache.nix new file mode 100644 index 000000000000..aec9415b333f --- /dev/null +++ b/nixos/tests/vinyl-cache.nix @@ -0,0 +1,126 @@ +{ + pkgs, + package, + lib, + ... +}: +let + testPath = pkgs.hello; +in +{ + name = "vinyl"; + meta = { + maintainers = [ + lib.maintainers.leona + lib.maintainers.osnyx + ]; + }; + + nodes = { + vinyl = + { + config, + pkgs, + lib, + ... + }: + { + services.nix-serve = { + enable = true; + }; + + services.vinyl-cache = { + inherit package; + enable = true; + enableFileLogging = true; + listen = [ + { + address = "0.0.0.0"; + port = 80; + proto = "HTTP"; + } + { + name = "proxyport"; + address = "0.0.0.0"; + port = 8080; + proto = "PROXY"; + } + { + address = "/var/run/vinyld/client.http.sock"; + user = "vinyl-cache"; + group = "vinyl-cache"; + mode = "660"; + } + ] + ++ lib.optionals (lib.versionAtLeast package.version "7.3") [ + # Support added in 7.3.0 + { address = "@asdf"; } + ]; + config = '' + vcl 4.1; + + backend nix-serve { + .host = "127.0.0.1"; + .port = "${toString config.services.nix-serve.port}"; + } + ''; + }; + + networking.firewall.allowedTCPPorts = [ 80 ]; + system.extraDependencies = [ testPath ]; + + assertions = + let + cmdline = config.systemd.services.vinyl-cache.serviceConfig.ExecStart; + in + map + (pattern: { + assertion = lib.hasInfix pattern cmdline; + message = "Address argument `${pattern}` missing in commandline `${cmdline}`."; + }) + ( + [ + " -a 0.0.0.0:80,HTTP " + " -a proxyport=0.0.0.0:8080,PROXY " + " -a /var/run/vinyld/client.http.sock,HTTP,user=vinyl-cache,group=vinyl-cache,mode=660 " + ] + ++ lib.optionals (lib.versionAtLeast package.version "7.3") [ + " -a @asdf,HTTP " + ] + ); + }; + + client = + { lib, ... }: + { + nix.settings = { + require-sigs = false; + substituters = lib.mkForce [ "http://vinyl" ]; + }; + }; + }; + + testScript = '' + from pathlib import Path + import os + + start_all() + vinyl.wait_for_open_port(80) + vinyl.wait_for_unit("vinylncsa") + + client.wait_until_succeeds("curl -f http://vinyl/nix-cache-info"); + + client.wait_until_succeeds("nix-store -r ${testPath}") + client.succeed("${testPath}/bin/hello") + + output = vinyl.succeed("vinyladm status") + print(output) + assert "Child in state running" in output, "Unexpected vinyladm response" + + vinyl.copy_from_machine("/var/log/vinyl-cache/vinyl-cache.log") + + out_dir = os.environ.get("out", os.getcwd()) + vinyl_log = (Path(out_dir) / "vinyl-cache.log").read_text() + assert "http://vinyl/nix-cache-info" in vinyl_log + ''; +} diff --git a/pkgs/servers/vinyl-cache/default.nix b/pkgs/servers/vinyl-cache/default.nix index 03aab5a100f3..11f4438ea787 100644 --- a/pkgs/servers/vinyl-cache/default.nix +++ b/pkgs/servers/vinyl-cache/default.nix @@ -108,6 +108,7 @@ let passthru = { python = python3; + tests = nixosTests."vinyl-cache_${lib.versions.major version}"; }; meta = {