commit 74a97921655193ddfe43f525060b7b6750a6da7d
parent 373ada0f2b0a78125c5f9c8f0579e59c15c7bcec
Author: Leah (ctucx) <git@ctu.cx>
Date: Sat, 13 May 2023 20:31:05 +0200
parent 373ada0f2b0a78125c5f9c8f0579e59c15c7bcec
Author: Leah (ctucx) <git@ctu.cx>
Date: Sat, 13 May 2023 20:31:05 +0200
machines/trabbi/git: refactor gitolite hooks (also fork the gitolite module from nixpkgs)
4 files changed, 296 insertions(+), 132 deletions(-)
M
|
189
+++++++++++++++++++++++++++++--------------------------------------------------
M
|
235
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
diff --git a/machines/trabbi/git.nix b/machines/trabbi/git.nix @@ -1,12 +1,7 @@ { config, lib, pkgs, ... }: let - rebuildScript = pkgs.writeShellScript "init-stagit" '' - systemctl start init-stagit; - systemctl status init-stagit; - ''; - - stagitFunctions = '' + stagitFunctions = pkgs.writeShellScript "stagitFunctions" '' export LC_CTYPE="en_US.UTF-8" is_public_and_listed() { @@ -16,6 +11,34 @@ let return 0 } + is_forced_update() { + test "$oldrev" = "0000000000000000000000000000000000000000" && return 1 + test "$newrev" = "0000000000000000000000000000000000000000" && return 1 + + hasrevs="$(${pkgs.git}/bin/git rev-list "$oldrev" "^$newrev" | ${pkgs.gnused}/bin/sed 1q)" + if test -n "$hasrevs"; then + return 0 + fi + return 1 + } + + make_stagit_repo() { + reponame="$(basename "$1" ".git")" + printf "[%s] Generate stagit HTML pages... " "$reponame" + + mkdir -p "/var/lib/stagit/$reponame" + cd "/var/lib/stagit/$reponame" || return 1 + + # make pages + ${pkgs.stagit}/bin/stagit -c '.stagit-build-cache' -n 'ctucx.git' -h 'https://git.ctu.cx/' -s 'git@${config.networking.hostName}.ctu.cx:' "$1" + + # set correct permissions + chown git:git -R /var/lib/stagit/$reponame; + chmod 755 -R /var/lib/stagit/$reponame; + + echo "done" + } + make_stagit_index() { printf "Generating stagit index... " @@ -39,11 +62,27 @@ let echo "$args" | xargs ${pkgs.stagit}/bin/stagit-index > /var/lib/stagit/index.html # set correct permissions - chmod 755 /var/lib/stagit/index.html; chown git:git /var/lib/stagit/index.html; + chmod 755 /var/lib/stagit/index.html; echo "done" } + + + update_stagit_repo() { + repo="$(pwd)" + reponame="$(basename "$repo" ".git")" + + cd "$repo" || return 1 + is_public_and_listed "$repo" || return 0 + + # if forced update, remove directory and cache file + is_forced_update && printf "[%s] Forced update, trigger complete regeneration of stagit-pages... \n" "$reponame" && rm -rf "/var/lib/stagit/$reponame" + + make_stagit_repo "$repo" + make_stagit_index + } + ''; in { @@ -61,136 +100,46 @@ in { paths = [ "/var/lib/gitolite" ]; }; - security.sudo.extraRules = [{ - users = [ "git" ]; - commands = [ - { command = "${rebuildScript}"; options = [ "SETENV" "NOPASSWD" ]; } - ]; - }]; - - services = { - }; - - systemd.services.init-stagit = { - script = '' - ${stagitFunctions} - - make_repo_web() { - reponame="$(basename "$1" ".git")" - printf "[%s] stagit HTML pages... " "$reponame" - - mkdir -p "/var/lib/stagit/$reponame" - cd "/var/lib/stagit/$reponame" || return 1 - - # make pages - ${pkgs.stagit}/bin/stagit -c '.stagit-build-cache' -n 'ctucx.git' -h 'https://git.ctu.cx/' -s 'git@${config.networking.hostName}.ctu.cx:' "$1" - - echo "done" - } - - # clean webdir - rm -rf /var/lib/stagit/* - - # make files per repo - for repo in "$HOME/repositories/"*.git/; do - repo="''${repo%/}" - is_public_and_listed "$repo" || continue - - make_repo_web "$repo" - done - - make_stagit_index - ''; - - serviceConfig = { - Type = "oneshot"; - - User = "git"; - Group = "git"; - - WorkingDirectory = "~"; - StateDirectory = "stagit"; - StateDirectoryMode = "755"; - - NoNewPrivileges = true; - PrivateTmp = true; - PrivateDevices = true; - - RestrictAddressFamilies = "AF_INET AF_INET6"; - RestrictNamespaces = true; - RestrictRealtime = true; - - ProtectSystem = "full"; - ProtectControlGroups = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - - DevicePolicy = "closed"; - LockPersonality = true; - }; - }; - services = { gitolite = { enable = true; user = "git"; group = "git"; adminPubkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDb2eZ2ymt+Zsf0eTlmjW2jPdS013lbde1+EGkgu6bz9lVTR8aawshF2HcoaWp5a5dJr3SKyihDM8hbWSYB3qyTHihNGyCArqSvAtZRw301ailRVHGqiwUITTfcg1533TtmWvlJZgOIFM1VvSAfdueDRRRzbygmn749fS9nhUTDzLtjqX5LvhpqhzsD+eOqPrV6Ne8E1e42JxQb5AJPY1gj9mk6eAarvtEHQYEe+/hp9ERjtCdN5DfuOJnqfaKS0ytPj/NbQskbX/TMgeUVio11iC2NbXsnAtzMmtbLX4mxlDQrR6aZmU/rHQ4aeJqI/Tj2rrF46icri7s0tnnit1OjT5PSxXgifcOtn06qoxYZMT1x+Dyrt40vNkGmxmxCnirm8B+6MKXgd/Ys+7tnOm1ht8TmLm96x6KdOiF3Zq/tMxhPAzp8JriTKSo7k7U9XxStFghTbhhBNc7OX89ZbpalLEnvbQiz87gZxhcx8cLvzIjslOHmZOSWC5Pgr4wwuj3Akq63i4ya6/BzM6v4UoBuDAB6fz3NHKL4R5X20la7Pvt7OBysQkGClWfj6ipMR1bFE2mfYtlMioXNgTjC+NCpEl1+81MH7dv2565Hk8CLV8FMxv6GujbAZGjjcM47lpWM1cBQvpBMUA/lLkyiCPK0YxNWAB7Co+jYDl6CR0Ubew== cardno:6445161"; + extraGitoliteRc = '' - $RC{UMASK} = 0027; $RC{GIT_CONFIG_KEYS} = ".*"; - push( @{$RC{ENABLE}}, 'cgit' ); - ''; - hooks.postReceive = '' - ${stagitFunctions} - - is_forced_update() { - test "$oldrev" = "0000000000000000000000000000000000000000" && return 1 - test "$newrev" = "0000000000000000000000000000000000000000" && return 1 + $RC{UMASK} = 0027; - hasrevs="$(${pkgs.git}/bin/git rev-list "$oldrev" "^$newrev" | ${pkgs.gnused}/bin/sed 1q)" - if test -n "$hasrevs"; then - return 0 - fi - return 1 - } + push(@{$RC{ENABLE}}, 'cgit'); + push(@{$RC{ENABLE}}, 'rebuild-stagit'); - make_repo_web() { - reponame="$(basename "$1" ".git")" - printf "[%s] stagit HTML pages... " "$reponame" - - # if forced update, remove directory and cache file - is_forced_update && printf "forced update... " && rm -rf "/var/lib/stagit/$reponame" - - mkdir -p "/var/lib/stagit/$reponame" - cd "/var/lib/stagit/$reponame" || return 1 - - # make pages - ${pkgs.stagit}/bin/stagit -c '.stagit-build-cache' -n 'ctucx.git' -h 'https://git.ctu.cx/' -s 'git@${config.networking.hostName}.ctu.cx:' "$1" + $RC{NON_CORE} = "rebuild-stagit POST_COMPILE rebuild-stagit"; + ''; - # set correct permissions - chmod 755 -R /var/lib/stagit/$reponame; - chown git:git -R /var/lib/stagit/$reponame; + triggers.rebuild-stagit = '' + source ${stagitFunctions} - echo "done" - } + # clear webdir + rm -rf /var/lib/stagit/* - update_stagit_repo() { - repo="$(pwd)" + # generate pages per repo + for repo in "$HOME/repositories/"*.git/; do + repo="''${repo%/}" + is_public_and_listed "$repo" || continue - cd "$repo" || return 1 - is_public_and_listed "$repo" || return 0 + make_stagit_repo "$repo" + done - make_repo_web "$repo" - make_stagit_index - } + # generate index page + make_stagit_index + ''; + commonHooks.post-receive = '' + # update stagit pages + source ${stagitFunctions} update_stagit_repo "$1" - - #rebuild stagit - [ "$GL_REPO" == "gitolite-admin" ] && sudo ${rebuildScript} ''; - }; fcgiwrap = { @@ -228,6 +177,8 @@ in { kTLS = true; root = "/var/lib/stagit"; locations = { + "@redir".return = "307 ../log.html"; + "~ '^/([a-zA-Z0-9_.]+)/commit/.*$'".extraConfig = "error_page 404 = @redir;"; "~ '^/[a-zA-Z0-9._-]+/raw'".extraConfig = '' types { application/json json;
diff --git a/machines/trabbi/websites/bikemap.ctu.cx.nix b/machines/trabbi/websites/bikemap.ctu.cx.nix @@ -76,7 +76,7 @@ in { }; services = { - gitolite.hooks.postReceive = '' + gitolite.commonHooks.post-receive = '' #deploy bikemap [ "$GL_REPO" == "biketracks" ] && sudo ${deployScript} '';
diff --git a/modules/default.nix b/modules/default.nix @@ -3,6 +3,8 @@ { + disabledModules = [ "services/misc/gitolite.nix" ]; + imports = (builtins.concatLists [ (if (currentSystem == "x86_64-linux") then [ inputs.agenix.nixosModules.default
diff --git a/modules/linux/gitolite.nix b/modules/linux/gitolite.nix @@ -1,29 +1,240 @@ -{ options, config, pkgs, lib, ... }: + +{ config, lib, pkgs, ... }: with lib; let cfg = config.services.gitolite; + # Use writeTextDir to not leak Nix store hash into file name + pubkeyFile = (pkgs.writeTextDir "gitolite-admin.pub" cfg.adminPubkey) + "/gitolite-admin.pub"; + hooks = lib.mapAttrs (name: script: ( + pkgs.writeShellScript name (if name == "post-receive" then '' + read oldrev newrev ref + [ -t 0 ] || cat >/dev/null + [ -z "$GL_REPO" ] && die GL_REPO not set + + '' + script else script) + )) cfg.commonHooks; + triggers = lib.mapAttrs (name: script: (pkgs.writeShellScript name script)) cfg.triggers; in { options = { - services.gitolite.hooks.postReceive = mkOption { - type = types.lines; - default = ""; + services.gitolite = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Enable gitolite management under the + `gitolite` user. After + switching to a configuration with Gitolite enabled, you can + then run `git clone gitolite@host:gitolite-admin.git` to manage it further. + ''; + }; + + dataDir = mkOption { + type = types.str; + default = "/var/lib/gitolite"; + description = lib.mdDoc '' + The gitolite home directory used to store all repositories. If left as the default value + this directory will automatically be created before the gitolite server starts, otherwise + the sysadmin is responsible for ensuring the directory exists with appropriate ownership + and permissions. + ''; + }; + + adminPubkey = mkOption { + type = types.str; + description = lib.mdDoc '' + Initial administrative public key for Gitolite. This should + be an SSH Public Key. Note that this key will only be used + once, upon the first initialization of the Gitolite user. + The key string cannot have any line breaks in it. + ''; + }; + + commonHooks = mkOption { + type = types.attrsOf types.lines; + default = {}; + }; + + triggers = mkOption { + type = types.attrsOf types.lines; + default = {}; + }; + + extraGitoliteRc = mkOption { + type = types.lines; + default = ""; + example = literalExpression '' + ''' + $RC{UMASK} = 0027; + $RC{SITE_INFO} = 'This is our private repository host'; + push( @{$RC{ENABLE}}, 'Kindergarten' ); # enable the command/feature + @{$RC{ENABLE}} = grep { $_ ne 'desc' } @{$RC{ENABLE}}; # disable the command/feature + ''' + ''; + description = lib.mdDoc '' + Extra configuration to append to the default `~/.gitolite.rc`. + + This should be Perl code that modifies the `%RC` + configuration variable. The default `~/.gitolite.rc` + content is generated by invoking `gitolite print-default-rc`, + and extra configuration from this option is appended to it. The result + is placed to Nix store, and the `~/.gitolite.rc` file + becomes a symlink to it. + + If you already have a customized (or otherwise changed) + `~/.gitolite.rc` file, NixOS will refuse to replace + it with a symlink, and the `gitolite-init` initialization service + will fail. In this situation, in order to use this option, you + will need to take any customizations you may have in + `~/.gitolite.rc`, convert them to appropriate Perl + statements, add them to this option, and remove the file. + + See also the `enableGitAnnex` option. + ''; + }; + + user = mkOption { + type = types.str; + default = "gitolite"; + description = lib.mdDoc '' + Gitolite user account. This is the username of the gitolite endpoint. + ''; + }; + + group = mkOption { + type = types.str; + default = "gitolite"; + description = lib.mdDoc '' + Primary group of the Gitolite user account. + ''; + }; }; }; - config = lib.mkIf cfg.enable { - services.gitolite.commonHooks = [ "${pkgs.writeShellScriptBin "post-receive" '' - read oldrev newrev ref - [ -t 0 ] || cat >/dev/null - [ -z "$GL_REPO" ] && die GL_REPO not set + config = mkIf cfg.enable ( + let + manageGitoliteRc = cfg.extraGitoliteRc != ""; + rcDir = pkgs.runCommand "gitolite-rc" { preferLocalBuild = true; } rcDirScript; + rcDirScript = + '' + mkdir "$out" + export HOME=temp-home + mkdir -p "$HOME/.gitolite/logs" # gitolite can't run without it + '${pkgs.gitolite}'/bin/gitolite print-default-rc >>"$out/gitolite.rc.default" + cat <<END >>"$out/gitolite.rc" + # This file is managed by NixOS. + # Use services.gitolite options to control it. - ${cfg.hooks.postReceive} + END + cat "$out/gitolite.rc.default" >>"$out/gitolite.rc" + '' + + optionalString (cfg.extraGitoliteRc != "") '' + echo -n ${escapeShellArg '' - ''}/bin/post-receive" ]; + # Added by NixOS: + ${removeSuffix "\n" cfg.extraGitoliteRc} - }; + # per perl rules, this should be the last line in such a file: + 1; + ''} >>"$out/gitolite.rc" + ''; + in { + + services.gitolite.extraGitoliteRc = '' + $RC{LOCAL_CODE} = "$ENV{HOME}/.gitolite/local"; + ''; + + users.users.${cfg.user} = { + home = cfg.dataDir; + uid = config.ids.uids.gitolite; + group = cfg.group; + useDefaultShell = true; + }; + + users.groups.${cfg.group}.gid = config.ids.gids.gitolite; + + systemd.services.gitolite-init = { + description = "Gitolite initialization"; + wantedBy = [ "multi-user.target" ]; + unitConfig.RequiresMountsFor = cfg.dataDir; + + environment = { + GITOLITE_RC = ".gitolite.rc"; + GITOLITE_RC_DEFAULT = "${rcDir}/gitolite.rc.default"; + }; + + serviceConfig = mkMerge [ + (mkIf (cfg.dataDir == "/var/lib/gitolite") { + StateDirectory = "gitolite gitolite/.gitolite gitolite/.gitolite/logs"; + StateDirectoryMode = "0750"; + }) + { + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = "~"; + RemainAfterExit = true; + } + ]; + + path = [ pkgs.gitolite pkgs.git pkgs.perl pkgs.bash pkgs.diffutils config.programs.ssh.package ]; + script = + let + rcSetupScriptIfCustomFile = + if manageGitoliteRc then '' + cat <<END + <3>ERROR: NixOS can't apply declarative configuration + <3>to your .gitolite.rc file, because it seems to be + <3>already customized manually. + <3>See the services.gitolite.extraGitoliteRc option + <3>in "man configuration.nix" for more information. + END + # Not sure if the line below addresses the issue directly or just + # adds a delay, but without it our error message often doesn't + # show up in `systemctl status gitolite-init`. + journalctl --flush + exit 1 + '' else '' + : + ''; + rcSetupScriptIfDefaultFileOrStoreSymlink = + if manageGitoliteRc then '' + ln -sf "${rcDir}/gitolite.rc" "$GITOLITE_RC" + '' else '' + [[ -L "$GITOLITE_RC" ]] && rm -f "$GITOLITE_RC" + ''; + in + '' + if ( [[ ! -e "$GITOLITE_RC" ]] && [[ ! -L "$GITOLITE_RC" ]] ) || + ( [[ -f "$GITOLITE_RC" ]] && diff -q "$GITOLITE_RC" "$GITOLITE_RC_DEFAULT" >/dev/null ) || + ( [[ -L "$GITOLITE_RC" ]] && [[ "$(readlink "$GITOLITE_RC")" =~ ^/nix/store/ ]] ) + then + '' + rcSetupScriptIfDefaultFileOrStoreSymlink + + '' + else + '' + rcSetupScriptIfCustomFile + + '' + fi + + if [ ! -d repositories ]; then + gitolite setup -pk ${pubkeyFile} + fi + + rm -rf .gitolite/local/hooks/common/ + mkdir -p .gitolite/local/hooks/common/ + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: script: "ln -s ${script} .gitolite/local/hooks/common/${name}") hooks)} + + rm -rf .gitolite/local/triggers + mkdir -p .gitolite/local/triggers + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: script: "ln -s ${script} .gitolite/local/triggers/${name}") triggers)} + + gitolite setup # Upgrade if needed + ''; + }; + environment.systemPackages = [ pkgs.gitolite pkgs.git ]; + }); }