lol

nixos/soft-serve: init

This adds a NixOS module for Soft Serve, a tasty, self-hostable Git
server for the command line. The module has a test that checks some
basic things like creating users, creating a repo and cloning it.

Co-authored-by: Sandro <sandro.jaeckel@gmail.com>

dadada 77f7b5a3 63ecebe2

+208 -1
+2
nixos/doc/manual/release-notes/rl-2311.section.md
··· 113 113 114 114 - [virt-manager](https://virt-manager.org/), an UI for managing virtual machines in libvirt, is now available as `programs.virt-manager`. 115 115 116 + - [Soft Serve](https://github.com/charmbracelet/soft-serve), a tasty, self-hostable Git server for the command line. Available as [services.soft-serve](#opt-services.soft-serve.enable). 117 + 116 118 ## Backward Incompatibilities {#sec-release-23.11-incompatibilities} 117 119 118 120 - `network-online.target` has been fixed to no longer time out for systems with `networking.useDHCP = true` and `networking.useNetworkd = true`.
+1
nixos/modules/module-list.nix
··· 730 730 ./services/misc/signald.nix 731 731 ./services/misc/siproxd.nix 732 732 ./services/misc/snapper.nix 733 + ./services/misc/soft-serve.nix 733 734 ./services/misc/sonarr.nix 734 735 ./services/misc/sourcehut 735 736 ./services/misc/spice-vdagentd.nix
+99
nixos/modules/services/misc/soft-serve.nix
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + 5 + let 6 + cfg = config.services.soft-serve; 7 + configFile = format.generate "config.yaml" cfg.settings; 8 + format = pkgs.formats.yaml { }; 9 + docUrl = "https://charm.sh/blog/self-hosted-soft-serve/"; 10 + stateDir = "/var/lib/soft-serve"; 11 + in 12 + { 13 + options = { 14 + services.soft-serve = { 15 + enable = mkEnableOption "Enable soft-serve service"; 16 + 17 + package = mkPackageOption pkgs "soft-serve" { }; 18 + 19 + settings = mkOption { 20 + type = format.type; 21 + default = { }; 22 + description = mdDoc '' 23 + The contents of the configuration file. 24 + 25 + See <${docUrl}>. 26 + ''; 27 + example = literalExpression '' 28 + { 29 + name = "dadada's repos"; 30 + log_format = "text"; 31 + ssh = { 32 + listen_addr = ":23231"; 33 + public_url = "ssh://localhost:23231"; 34 + max_timeout = 30; 35 + idle_timeout = 120; 36 + }; 37 + stats.listen_addr = ":23233"; 38 + } 39 + ''; 40 + }; 41 + }; 42 + }; 43 + 44 + config = mkIf cfg.enable { 45 + 46 + systemd.tmpfiles.rules = [ 47 + # The config file has to be inside the state dir 48 + "L+ ${stateDir}/config.yaml - - - - ${configFile}" 49 + ]; 50 + 51 + systemd.services.soft-serve = { 52 + description = "Soft Serve git server"; 53 + documentation = [ docUrl ]; 54 + requires = [ "network-online.target" ]; 55 + after = [ "network-online.target" ]; 56 + wantedBy = [ "multi-user.target" ]; 57 + 58 + environment.SOFT_SERVE_DATA_PATH = stateDir; 59 + 60 + serviceConfig = { 61 + Type = "simple"; 62 + DynamicUser = true; 63 + Restart = "always"; 64 + ExecStart = "${getExe cfg.package} serve"; 65 + StateDirectory = "soft-serve"; 66 + WorkingDirectory = stateDir; 67 + RuntimeDirectory = "soft-serve"; 68 + RuntimeDirectoryMode = "0750"; 69 + ProcSubset = "pid"; 70 + ProtectProc = "invisible"; 71 + UMask = "0027"; 72 + CapabilityBoundingSet = ""; 73 + ProtectHome = true; 74 + PrivateDevices = true; 75 + PrivateUsers = true; 76 + ProtectHostname = true; 77 + ProtectClock = true; 78 + ProtectKernelTunables = true; 79 + ProtectKernelModules = true; 80 + ProtectKernelLogs = true; 81 + ProtectControlGroups = true; 82 + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; 83 + RestrictNamespaces = true; 84 + LockPersonality = true; 85 + MemoryDenyWriteExecute = true; 86 + RestrictRealtime = true; 87 + RemoveIPC = true; 88 + PrivateMounts = true; 89 + SystemCallArchitectures = "native"; 90 + SystemCallFilter = [ 91 + "@system-service" 92 + "~@cpu-emulation @debug @keyring @module @mount @obsolete @privileged @raw-io @reboot @setuid @swap" 93 + ]; 94 + }; 95 + }; 96 + }; 97 + 98 + meta.maintainers = [ maintainers.dadada ]; 99 + }
+1
nixos/tests/all-tests.nix
··· 732 732 snapper = handleTest ./snapper.nix {}; 733 733 snipe-it = runTest ./web-apps/snipe-it.nix; 734 734 soapui = handleTest ./soapui.nix {}; 735 + soft-serve = handleTest ./soft-serve.nix {}; 735 736 sogo = handleTest ./sogo.nix {}; 736 737 solanum = handleTest ./solanum.nix {}; 737 738 sonarr = handleTest ./sonarr.nix {};
+102
nixos/tests/soft-serve.nix
··· 1 + import ./make-test-python.nix ({ pkgs, lib, ... }: 2 + let 3 + inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; 4 + sshPort = 8231; 5 + httpPort = 8232; 6 + statsPort = 8233; 7 + gitPort = 8418; 8 + in 9 + { 10 + name = "soft-serve"; 11 + meta.maintainers = with lib.maintainers; [ dadada ]; 12 + nodes = { 13 + client = { pkgs, ... }: { 14 + environment.systemPackages = with pkgs; [ 15 + curl 16 + git 17 + openssh 18 + ]; 19 + environment.etc.sshKey = { 20 + source = snakeOilPrivateKey; 21 + mode = "0600"; 22 + }; 23 + }; 24 + 25 + server = 26 + { config, ... }: 27 + { 28 + services.soft-serve = { 29 + enable = true; 30 + settings = { 31 + name = "TestServer"; 32 + ssh.listen_addr = ":${toString sshPort}"; 33 + git.listen_addr = ":${toString gitPort}"; 34 + http.listen_addr = ":${toString httpPort}"; 35 + stats.listen_addr = ":${toString statsPort}"; 36 + initial_admin_keys = [ snakeOilPublicKey ]; 37 + }; 38 + }; 39 + networking.firewall.allowedTCPPorts = [ sshPort httpPort statsPort ]; 40 + }; 41 + }; 42 + 43 + testScript = 44 + { ... }: 45 + '' 46 + SSH_PORT = ${toString sshPort} 47 + HTTP_PORT = ${toString httpPort} 48 + STATS_PORT = ${toString statsPort} 49 + KEY = "${snakeOilPublicKey}" 50 + SSH_KEY = "/etc/sshKey" 51 + SSH_COMMAND = f"ssh -p {SSH_PORT} -i {SSH_KEY} -o StrictHostKeyChecking=no" 52 + TEST_DIR = "/tmp/test" 53 + GIT = f"git -C {TEST_DIR}" 54 + 55 + for machine in client, server: 56 + machine.wait_for_unit("network.target") 57 + 58 + server.wait_for_unit("soft-serve.service") 59 + server.wait_for_open_port(SSH_PORT) 60 + 61 + with subtest("Get info"): 62 + status, test = client.execute(f"{SSH_COMMAND} server info") 63 + if status != 0: 64 + raise Exception("Failed to get SSH info") 65 + key = " ".join(KEY.split(" ")[0:2]) 66 + if not key in test: 67 + raise Exception("Admin key must be configured correctly") 68 + 69 + with subtest("Create user"): 70 + client.succeed(f"{SSH_COMMAND} server user create beatrice") 71 + client.succeed(f"{SSH_COMMAND} server user info beatrice") 72 + 73 + with subtest("Create repo"): 74 + client.succeed(f"git init {TEST_DIR}") 75 + client.succeed(f"{GIT} config --global user.email you@example.com") 76 + client.succeed(f"touch {TEST_DIR}/foo") 77 + client.succeed(f"{GIT} add foo") 78 + client.succeed(f"{GIT} commit --allow-empty -m test") 79 + client.succeed(f"{GIT} remote add origin git@server:test") 80 + client.succeed(f"GIT_SSH_COMMAND='{SSH_COMMAND}' {GIT} push -u origin master") 81 + client.execute("rm -r /tmp/test") 82 + 83 + server.wait_for_open_port(HTTP_PORT) 84 + 85 + with subtest("Clone over HTTP"): 86 + client.succeed(f"curl --connect-timeout 10 http://server:{HTTP_PORT}/") 87 + client.succeed(f"git clone http://server:{HTTP_PORT}/test /tmp/test") 88 + client.execute("rm -r /tmp/test") 89 + 90 + with subtest("Clone over SSH"): 91 + client.succeed(f"GIT_SSH_COMMAND='{SSH_COMMAND}' git clone git@server:test /tmp/test") 92 + client.execute("rm -r /tmp/test") 93 + 94 + with subtest("Get stats over HTTP"): 95 + server.wait_for_open_port(STATS_PORT) 96 + status, test = client.execute(f"curl --connect-timeout 10 http://server:{STATS_PORT}/metrics") 97 + if status != 0: 98 + raise Exception("Failed to get metrics from status port") 99 + if not "go_gc_duration_seconds_count" in test: 100 + raise Exception("Metrics did not contain key 'go_gc_duration_seconds_count'") 101 + ''; 102 + })
+3 -1
pkgs/servers/soft-serve/default.nix
··· 1 - { lib, buildGoModule, fetchFromGitHub, makeWrapper, git, bash }: 1 + { lib, buildGoModule, fetchFromGitHub, makeWrapper, nixosTests, git, bash }: 2 2 3 3 buildGoModule rec { 4 4 pname = "soft-serve"; ··· 25 25 wrapProgram $out/bin/soft \ 26 26 --prefix PATH : "${lib.makeBinPath [ git bash ]}" 27 27 ''; 28 + 29 + passthru.tests = nixosTests.soft-serve; 28 30 29 31 meta = with lib; { 30 32 description = "A tasty, self-hosted Git server for the command line";