Merge pull request #242467 from dadada/dev/dadada/init-nixos-soft-serve

nixos/soft-serve: init

Janik abca224c 13b98764

Changed files
+211 -2
nixos
doc
manual
release-notes
modules
tests
pkgs
servers
soft-serve
+2
nixos/doc/manual/release-notes/rl-2311.section.md
···
- [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`.
+1
nixos/modules/module-list.nix
···
./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
+99
nixos/modules/services/misc/soft-serve.nix
···
+
{ 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 ];
+
}
+1
nixos/tests/all-tests.nix
···
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 {};
+102
nixos/tests/soft-serve.nix
···
+
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'")
+
'';
+
})
+6 -2
pkgs/servers/soft-serve/default.nix
···
-
{ lib, buildGoModule, fetchFromGitHub, makeWrapper, git }:
+
{ lib, buildGoModule, fetchFromGitHub, makeWrapper, nixosTests, git, bash }:
buildGoModule rec {
pname = "soft-serve";
···
nativeBuildInputs = [ makeWrapper ];
postInstall = ''
+
# Soft-serve generates git-hooks at run-time.
+
# The scripts require git and bash inside the path.
wrapProgram $out/bin/soft \
-
--prefix PATH : "${lib.makeBinPath [ git ]}"
+
--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";