From 218c7795a669d577fc3ae79a571135e5f105793a Mon Sep 17 00:00:00 2001 From: schnusch Date: Tue, 5 Apr 2022 02:02:11 +0200 Subject: [PATCH] nixos/cgit: init --- nixos/modules/module-list.nix | 1 + nixos/modules/services/networking/cgit.nix | 201 +++++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/cgit.nix | 59 ++++++ 4 files changed, 262 insertions(+) create mode 100644 nixos/modules/services/networking/cgit.nix create mode 100644 nixos/tests/cgit.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 5e520c8308c..0f5c778a6f3 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -793,6 +793,7 @@ ./services/networking/bitlbee.nix ./services/networking/blockbook-frontend.nix ./services/networking/blocky.nix + ./services/networking/cgit.nix ./services/networking/charybdis.nix ./services/networking/chisel-server.nix ./services/networking/cjdns.nix diff --git a/nixos/modules/services/networking/cgit.nix b/nixos/modules/services/networking/cgit.nix new file mode 100644 index 00000000000..d2db1c294e6 --- /dev/null +++ b/nixos/modules/services/networking/cgit.nix @@ -0,0 +1,201 @@ +{ config, lib, pkgs, ...}: + +with lib; + +let + cfgs = config.services.cgit; + + settingType = with types; oneOf [ bool int str ]; + + genAttrs' = names: f: listToAttrs (map f names); + + regexEscape = + let + # taken from https://github.com/python/cpython/blob/05cb728d68a278d11466f9a6c8258d914135c96c/Lib/re.py#L251-L266 + special = [ + "(" ")" "[" "]" "{" "}" "?" "*" "+" "-" "|" "^" "$" "\\" "." "&" "~" + "#" " " "\t" "\n" "\r" "\v" "\f" + ]; + in + replaceStrings special (map (c: "\\${c}") special); + + stripLocation = cfg: removeSuffix "/" cfg.nginx.location; + + regexLocation = cfg: regexEscape (stripLocation cfg); + + mkFastcgiPass = cfg: '' + ${if cfg.nginx.location == "/" then '' + fastcgi_param PATH_INFO $uri; + '' else '' + fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$; + fastcgi_param PATH_INFO $fastcgi_path_info; + '' + }fastcgi_pass unix:${config.services.fcgiwrap.socketAddress}; + ''; + + cgitrcLine = name: value: "${name}=${ + if value == true then + "1" + else if value == false then + "0" + else + toString value + }"; + + mkCgitrc = cfg: pkgs.writeText "cgitrc" '' + # global settings + ${concatStringsSep "\n" ( + mapAttrsToList + cgitrcLine + ({ virtual-root = cfg.nginx.location; } // cfg.settings) + ) + } + ${optionalString (cfg.scanPath != null) (cgitrcLine "scan-path" cfg.scanPath)} + + # repository settings + ${concatStrings ( + mapAttrsToList + (url: settings: '' + ${cgitrcLine "repo.url" url} + ${concatStringsSep "\n" ( + mapAttrsToList (name: cgitrcLine "repo.${name}") settings + ) + } + '') + cfg.repos + ) + } + + # extra config + ${cfg.extraConfig} + ''; + + mkCgitReposDir = cfg: + if cfg.scanPath != null then + cfg.scanPath + else + pkgs.runCommand "cgit-repos" { + preferLocalBuild = true; + allowSubstitutes = false; + } '' + mkdir -p "$out" + ${ + concatStrings ( + mapAttrsToList + (name: value: '' + ln -s ${escapeShellArg value.path} "$out"/${escapeShellArg name} + '') + cfg.repos + ) + } + ''; + +in +{ + options = { + services.cgit = mkOption { + description = mdDoc "Configure cgit instances."; + default = {}; + type = types.attrsOf (types.submodule ({ config, ... }: { + options = { + enable = mkEnableOption (mdDoc "cgit"); + + nginx.virtualHost = mkOption { + description = mdDoc "VirtualHost to serve cgit on, defaults to the attribute name."; + type = types.str; + default = config._module.args.name; + example = "git.example.com"; + }; + + nginx.location = mkOption { + description = mdDoc "Location to serve cgit under."; + type = types.str; + default = "/"; + example = "/git/"; + }; + + repos = mkOption { + description = mdDoc "cgit repository settings, see cgitrc(5)"; + type = with types; attrsOf (attrsOf settingType); + default = {}; + example = { + blah = { + path = "/var/lib/git/example"; + desc = "An example repository"; + }; + }; + }; + + scanPath = mkOption { + description = mdDoc "A path which will be scanned for repositories."; + type = types.nullOr types.path; + default = null; + example = "/var/lib/git"; + }; + + settings = mkOption { + description = mdDoc "cgit configuration, see cgitrc(5)"; + type = types.attrsOf settingType; + default = {}; + example = literalExpression '' + { + enable-follow-links = true; + source-filter = "''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py"; + } + ''; + }; + + extraConfig = mkOption { + description = mdDoc "These lines go to the end of cgitrc verbatim."; + type = types.lines; + default = ""; + }; + }; + })); + }; + }; + + config = mkIf (any (cfg: cfg.enable) (attrValues cfgs)) { + assertions = mapAttrsToList (vhost: cfg: { + assertion = !cfg.enable || (cfg.scanPath == null) != (cfg.repos == {}); + message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set."; + }) cfgs; + + services.fcgiwrap.enable = true; + + services.nginx.enable = true; + + services.nginx.virtualHosts = mkMerge (mapAttrsToList (_: cfg: { + ${cfg.nginx.virtualHost} = { + locations = ( + genAttrs' + [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ] + (name: nameValuePair "= ${stripLocation cfg}/${name}" { + extraConfig = '' + alias ${pkgs.cgit}/cgit/${name}; + ''; + }) + ) // { + "~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = { + fastcgiParams = rec { + SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend"; + GIT_HTTP_EXPORT_ALL = "1"; + GIT_PROJECT_ROOT = mkCgitReposDir cfg; + HOME = GIT_PROJECT_ROOT; + }; + extraConfig = mkFastcgiPass cfg; + }; + "${stripLocation cfg}/" = { + fastcgiParams = { + SCRIPT_FILENAME = "${pkgs.cgit}/cgit/cgit.cgi"; + QUERY_STRING = "$args"; + HTTP_HOST = "$server_name"; + CGIT_CONFIG = mkCgitrc cfg; + }; + extraConfig = mkFastcgiPass cfg; + }; + }; + }; + }) cfgs); + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index b4bd8ef3e0f..fff75561310 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -124,6 +124,7 @@ in { ceph-single-node-bluestore = handleTestOn ["x86_64-linux"] ./ceph-single-node-bluestore.nix {}; certmgr = handleTest ./certmgr.nix {}; cfssl = handleTestOn ["aarch64-linux" "x86_64-linux"] ./cfssl.nix {}; + cgit = handleTest ./cgit.nix {}; charliecloud = handleTest ./charliecloud.nix {}; chromium = (handleTestOn ["aarch64-linux" "x86_64-linux"] ./chromium.nix {}).stable or {}; cinnamon = handleTest ./cinnamon.nix {}; diff --git a/nixos/tests/cgit.nix b/nixos/tests/cgit.nix new file mode 100644 index 00000000000..207f7d13fd8 --- /dev/null +++ b/nixos/tests/cgit.nix @@ -0,0 +1,59 @@ +import ./make-test-python.nix ({ pkgs, ...} : { + name = "cgit"; + meta = with pkgs.lib.maintainers; { + maintainers = [ schnusch ]; + }; + + nodes = { + server = { ... }: { + services.cgit."localhost" = { + enable = true; + nginx.location = "/(c)git/"; + repos = { + some-repo = { + path = "/srv/git/some-repo"; + desc = "some-repo description"; + }; + }; + }; + + environment.systemPackages = [ pkgs.git ]; + }; + }; + + testScript = { nodes, ... }: '' + start_all() + + server.wait_for_unit("nginx.service") + server.wait_for_unit("network.target") + server.wait_for_open_port(80) + + server.succeed("curl -fsS http://localhost/%28c%29git/robots.txt") + + server.succeed( + "curl -fsS http://localhost/%28c%29git/ | grep -F 'some-repo description'" + ) + + server.fail("curl -fsS http://localhost/robots.txt") + + server.succeed("${pkgs.writeShellScript "setup-cgit-test-repo" '' + set -e + git init --bare -b master /srv/git/some-repo + git init -b master reference + cd reference + git remote add origin /srv/git/some-repo + date > date.txt + git add date.txt + git -c user.name=test -c user.email=test@localhost commit -m 'add date' + git push -u origin master + ''}") + + server.succeed( + "curl -fsS 'http://localhost/%28c%29git/some-repo/plain/date.txt?id=master' | diff -u reference/date.txt -" + ) + + server.succeed( + "git clone http://localhost/%28c%29git/some-repo && diff -u reference/date.txt some-repo/date.txt" + ) + ''; +})