ctucx.git: nixfiles

ctucx' nixfiles

commit 74a97921655193ddfe43f525060b7b6750a6da7d
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
machines/trabbi/git.nix
|
189
+++++++++++++++++++++++++++++--------------------------------------------------
M
machines/trabbi/websites/bikemap.ctu.cx.nix
|
2
+-
M
modules/default.nix
|
2
++
M
modules/linux/gitolite.nix
|
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 ];
+  });
 }