From 77f7b5a3e5cf65b4b31204aae8d205a771ec990e Mon Sep 17 00:00:00 2001 From: dadada Date: Sat, 8 Jul 2023 01:44:42 +0200 Subject: [PATCH] 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 --- .../manual/release-notes/rl-2311.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/soft-serve.nix | 99 +++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/soft-serve.nix | 102 ++++++++++++++++++ pkgs/servers/soft-serve/default.nix | 4 +- 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 nixos/modules/services/misc/soft-serve.nix create mode 100644 nixos/tests/soft-serve.nix diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md index 38c89668f84a..13648364e13e 100644 --- a/nixos/doc/manual/release-notes/rl-2311.section.md +++ b/nixos/doc/manual/release-notes/rl-2311.section.md @@ -113,6 +113,8 @@ - [virt-manager](https://virt-manager.org/), an UI for managing virtual machines in libvirt, is now available as `programs.virt-manager`. +- [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). + ## Backward Incompatibilities {#sec-release-23.11-incompatibilities} - `network-online.target` has been fixed to no longer time out for systems with `networking.useDHCP = true` and `networking.useNetworkd = true`. diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 79918f71f7be..69f2a5a557c1 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -730,6 +730,7 @@ ./services/misc/signald.nix ./services/misc/siproxd.nix ./services/misc/snapper.nix + ./services/misc/soft-serve.nix ./services/misc/sonarr.nix ./services/misc/sourcehut ./services/misc/spice-vdagentd.nix diff --git a/nixos/modules/services/misc/soft-serve.nix b/nixos/modules/services/misc/soft-serve.nix new file mode 100644 index 000000000000..0f246493880b --- /dev/null +++ b/nixos/modules/services/misc/soft-serve.nix @@ -0,0 +1,99 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.soft-serve; + configFile = format.generate "config.yaml" cfg.settings; + format = pkgs.formats.yaml { }; + docUrl = "https://charm.sh/blog/self-hosted-soft-serve/"; + stateDir = "/var/lib/soft-serve"; +in +{ + options = { + services.soft-serve = { + enable = mkEnableOption "Enable soft-serve service"; + + package = mkPackageOption pkgs "soft-serve" { }; + + settings = mkOption { + type = format.type; + default = { }; + description = mdDoc '' + The contents of the configuration file. + + See <${docUrl}>. + ''; + example = literalExpression '' + { + name = "dadada's repos"; + log_format = "text"; + ssh = { + listen_addr = ":23231"; + public_url = "ssh://localhost:23231"; + max_timeout = 30; + idle_timeout = 120; + }; + stats.listen_addr = ":23233"; + } + ''; + }; + }; + }; + + config = mkIf cfg.enable { + + systemd.tmpfiles.rules = [ + # The config file has to be inside the state dir + "L+ ${stateDir}/config.yaml - - - - ${configFile}" + ]; + + systemd.services.soft-serve = { + description = "Soft Serve git server"; + documentation = [ docUrl ]; + requires = [ "network-online.target" ]; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment.SOFT_SERVE_DATA_PATH = stateDir; + + serviceConfig = { + Type = "simple"; + DynamicUser = true; + Restart = "always"; + ExecStart = "${getExe cfg.package} serve"; + StateDirectory = "soft-serve"; + WorkingDirectory = stateDir; + RuntimeDirectory = "soft-serve"; + RuntimeDirectoryMode = "0750"; + ProcSubset = "pid"; + ProtectProc = "invisible"; + UMask = "0027"; + CapabilityBoundingSet = ""; + ProtectHome = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RemoveIPC = true; + PrivateMounts = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@cpu-emulation @debug @keyring @module @mount @obsolete @privileged @raw-io @reboot @setuid @swap" + ]; + }; + }; + }; + + meta.maintainers = [ maintainers.dadada ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 33f8abf6ccd4..59da5827a1be 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -732,6 +732,7 @@ in { snapper = handleTest ./snapper.nix {}; snipe-it = runTest ./web-apps/snipe-it.nix; soapui = handleTest ./soapui.nix {}; + soft-serve = handleTest ./soft-serve.nix {}; sogo = handleTest ./sogo.nix {}; solanum = handleTest ./solanum.nix {}; sonarr = handleTest ./sonarr.nix {}; diff --git a/nixos/tests/soft-serve.nix b/nixos/tests/soft-serve.nix new file mode 100644 index 000000000000..1c4cb4c95819 --- /dev/null +++ b/nixos/tests/soft-serve.nix @@ -0,0 +1,102 @@ +import ./make-test-python.nix ({ pkgs, lib, ... }: +let + inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; + sshPort = 8231; + httpPort = 8232; + statsPort = 8233; + gitPort = 8418; +in +{ + name = "soft-serve"; + meta.maintainers = with lib.maintainers; [ dadada ]; + nodes = { + client = { pkgs, ... }: { + environment.systemPackages = with pkgs; [ + curl + git + openssh + ]; + environment.etc.sshKey = { + source = snakeOilPrivateKey; + mode = "0600"; + }; + }; + + server = + { config, ... }: + { + services.soft-serve = { + enable = true; + settings = { + name = "TestServer"; + ssh.listen_addr = ":${toString sshPort}"; + git.listen_addr = ":${toString gitPort}"; + http.listen_addr = ":${toString httpPort}"; + stats.listen_addr = ":${toString statsPort}"; + initial_admin_keys = [ snakeOilPublicKey ]; + }; + }; + networking.firewall.allowedTCPPorts = [ sshPort httpPort statsPort ]; + }; + }; + + testScript = + { ... }: + '' + SSH_PORT = ${toString sshPort} + HTTP_PORT = ${toString httpPort} + STATS_PORT = ${toString statsPort} + KEY = "${snakeOilPublicKey}" + SSH_KEY = "/etc/sshKey" + SSH_COMMAND = f"ssh -p {SSH_PORT} -i {SSH_KEY} -o StrictHostKeyChecking=no" + TEST_DIR = "/tmp/test" + GIT = f"git -C {TEST_DIR}" + + for machine in client, server: + machine.wait_for_unit("network.target") + + server.wait_for_unit("soft-serve.service") + server.wait_for_open_port(SSH_PORT) + + with subtest("Get info"): + status, test = client.execute(f"{SSH_COMMAND} server info") + if status != 0: + raise Exception("Failed to get SSH info") + key = " ".join(KEY.split(" ")[0:2]) + if not key in test: + raise Exception("Admin key must be configured correctly") + + with subtest("Create user"): + client.succeed(f"{SSH_COMMAND} server user create beatrice") + client.succeed(f"{SSH_COMMAND} server user info beatrice") + + with subtest("Create repo"): + client.succeed(f"git init {TEST_DIR}") + client.succeed(f"{GIT} config --global user.email you@example.com") + client.succeed(f"touch {TEST_DIR}/foo") + client.succeed(f"{GIT} add foo") + client.succeed(f"{GIT} commit --allow-empty -m test") + client.succeed(f"{GIT} remote add origin git@server:test") + client.succeed(f"GIT_SSH_COMMAND='{SSH_COMMAND}' {GIT} push -u origin master") + client.execute("rm -r /tmp/test") + + server.wait_for_open_port(HTTP_PORT) + + with subtest("Clone over HTTP"): + client.succeed(f"curl --connect-timeout 10 http://server:{HTTP_PORT}/") + client.succeed(f"git clone http://server:{HTTP_PORT}/test /tmp/test") + client.execute("rm -r /tmp/test") + + with subtest("Clone over SSH"): + client.succeed(f"GIT_SSH_COMMAND='{SSH_COMMAND}' git clone git@server:test /tmp/test") + client.execute("rm -r /tmp/test") + + with subtest("Get stats over HTTP"): + server.wait_for_open_port(STATS_PORT) + status, test = client.execute(f"curl --connect-timeout 10 http://server:{STATS_PORT}/metrics") + if status != 0: + raise Exception("Failed to get metrics from status port") + if not "go_gc_duration_seconds_count" in test: + raise Exception("Metrics did not contain key 'go_gc_duration_seconds_count'") + ''; +}) diff --git a/pkgs/servers/soft-serve/default.nix b/pkgs/servers/soft-serve/default.nix index 01a5ea9d6dd0..2cfd41f7caf8 100644 --- a/pkgs/servers/soft-serve/default.nix +++ b/pkgs/servers/soft-serve/default.nix @@ -1,4 +1,4 @@ -{ lib, buildGoModule, fetchFromGitHub, makeWrapper, git, bash }: +{ lib, buildGoModule, fetchFromGitHub, makeWrapper, nixosTests, git, bash }: buildGoModule rec { pname = "soft-serve"; @@ -26,6 +26,8 @@ buildGoModule rec { --prefix PATH : "${lib.makeBinPath [ git bash ]}" ''; + passthru.tests = nixosTests.soft-serve; + meta = with lib; { description = "A tasty, self-hosted Git server for the command line"; homepage = "https://github.com/charmbracelet/soft-serve";