mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-11-30 02:42:59 +00:00
Merge pull request #314440 from ju1m/radicle
This commit is contained in:
commit
51fcc2c92e
@ -24,6 +24,8 @@
|
|||||||
|
|
||||||
- [Eintopf](https://eintopf.info), community event and calendar web application. Available as [services.eintopf](options.html#opt-services.eintopf).
|
- [Eintopf](https://eintopf.info), community event and calendar web application. Available as [services.eintopf](options.html#opt-services.eintopf).
|
||||||
|
|
||||||
|
- [Radicle](https://radicle.xyz), an open source, peer-to-peer code collaboration stack built on Git. Available as [services.radicle](#opt-services.radicle.enable).
|
||||||
|
|
||||||
- [Renovate](https://github.com/renovatebot/renovate), a dependency updating tool for various git forges and language ecosystems. Available as [services.renovate](#opt-services.renovate.enable).
|
- [Renovate](https://github.com/renovatebot/renovate), a dependency updating tool for various git forges and language ecosystems. Available as [services.renovate](#opt-services.renovate.enable).
|
||||||
|
|
||||||
- [wg-access-server](https://github.com/freifunkMUC/wg-access-server/), an all-in-one WireGuard VPN solution with a web ui for connecting devices. Available at [services.wg-access-server](#opt-services.wg-access-server.enable).
|
- [wg-access-server](https://github.com/freifunkMUC/wg-access-server/), an all-in-one WireGuard VPN solution with a web ui for connecting devices. Available at [services.wg-access-server](#opt-services.wg-access-server.enable).
|
||||||
|
@ -803,6 +803,7 @@
|
|||||||
./services/misc/pufferpanel.nix
|
./services/misc/pufferpanel.nix
|
||||||
./services/misc/pykms.nix
|
./services/misc/pykms.nix
|
||||||
./services/misc/radarr.nix
|
./services/misc/radarr.nix
|
||||||
|
./services/misc/radicle.nix
|
||||||
./services/misc/readarr.nix
|
./services/misc/readarr.nix
|
||||||
./services/misc/redmine.nix
|
./services/misc/redmine.nix
|
||||||
./services/misc/renovate.nix
|
./services/misc/renovate.nix
|
||||||
|
347
nixos/modules/services/misc/radicle.nix
Normal file
347
nixos/modules/services/misc/radicle.nix
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
cfg = config.services.radicle;
|
||||||
|
|
||||||
|
json = pkgs.formats.json { };
|
||||||
|
|
||||||
|
env = rec {
|
||||||
|
# rad fails if it cannot stat $HOME/.gitconfig
|
||||||
|
HOME = "/var/lib/radicle";
|
||||||
|
RAD_HOME = HOME;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Convenient wrapper to run `rad` in the namespaces of `radicle-node.service`
|
||||||
|
rad-system = pkgs.writeShellScriptBin "rad-system" ''
|
||||||
|
set -o allexport
|
||||||
|
${toShellVars env}
|
||||||
|
# Note that --env is not used to preserve host's envvars like $TERM
|
||||||
|
exec ${getExe' pkgs.util-linux "nsenter"} -a \
|
||||||
|
-t "$(${getExe' config.systemd.package "systemctl"} show -P MainPID radicle-node.service)" \
|
||||||
|
-S "$(${getExe' config.systemd.package "systemctl"} show -P UID radicle-node.service)" \
|
||||||
|
-G "$(${getExe' config.systemd.package "systemctl"} show -P GID radicle-node.service)" \
|
||||||
|
${getExe' cfg.package "rad"} "$@"
|
||||||
|
'';
|
||||||
|
|
||||||
|
commonServiceConfig = serviceName: {
|
||||||
|
environment = env // {
|
||||||
|
RUST_LOG = mkDefault "info";
|
||||||
|
};
|
||||||
|
path = [
|
||||||
|
pkgs.gitMinimal
|
||||||
|
];
|
||||||
|
documentation = [
|
||||||
|
"https://docs.radicle.xyz/guides/seeder"
|
||||||
|
];
|
||||||
|
after = [
|
||||||
|
"network.target"
|
||||||
|
"network-online.target"
|
||||||
|
];
|
||||||
|
requires = [
|
||||||
|
"network-online.target"
|
||||||
|
];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = mkMerge [
|
||||||
|
{
|
||||||
|
BindReadOnlyPaths = [
|
||||||
|
"${cfg.configFile}:${env.RAD_HOME}/config.json"
|
||||||
|
"${if isPath cfg.publicKeyFile then cfg.publicKeyFile else pkgs.writeText "radicle.pub" cfg.publicKeyFile}:${env.RAD_HOME}/keys/radicle.pub"
|
||||||
|
];
|
||||||
|
KillMode = "process";
|
||||||
|
StateDirectory = [ "radicle" ];
|
||||||
|
User = config.users.users.radicle.name;
|
||||||
|
Group = config.users.groups.radicle.name;
|
||||||
|
WorkingDirectory = env.HOME;
|
||||||
|
}
|
||||||
|
# The following options are only for optimizing:
|
||||||
|
# systemd-analyze security ${serviceName}
|
||||||
|
{
|
||||||
|
BindReadOnlyPaths = [
|
||||||
|
"-/etc/resolv.conf"
|
||||||
|
"/etc/ssl/certs/ca-certificates.crt"
|
||||||
|
"/run/systemd"
|
||||||
|
];
|
||||||
|
AmbientCapabilities = "";
|
||||||
|
CapabilityBoundingSet = "";
|
||||||
|
DeviceAllow = ""; # ProtectClock= adds DeviceAllow=char-rtc r
|
||||||
|
LockPersonality = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProcSubset = "pid";
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectProc = "invisible";
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
RemoveIPC = true;
|
||||||
|
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RuntimeDirectoryMode = "700";
|
||||||
|
SocketBindDeny = [ "any" ];
|
||||||
|
StateDirectoryMode = "0750";
|
||||||
|
SystemCallFilter = [
|
||||||
|
"@system-service"
|
||||||
|
"~@aio"
|
||||||
|
"~@chown"
|
||||||
|
"~@keyring"
|
||||||
|
"~@memlock"
|
||||||
|
"~@privileged"
|
||||||
|
"~@resources"
|
||||||
|
"~@setuid"
|
||||||
|
"~@timer"
|
||||||
|
];
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
# This is for BindPaths= and BindReadOnlyPaths=
|
||||||
|
# to allow traversal of directories they create inside RootDirectory=
|
||||||
|
UMask = "0066";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
confinement = {
|
||||||
|
enable = true;
|
||||||
|
mode = "full-apivfs";
|
||||||
|
packages = [
|
||||||
|
pkgs.gitMinimal
|
||||||
|
cfg.package
|
||||||
|
pkgs.iana-etc
|
||||||
|
(getLib pkgs.nss)
|
||||||
|
pkgs.tzdata
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
services.radicle = {
|
||||||
|
enable = mkEnableOption "Radicle Seed Node";
|
||||||
|
package = mkPackageOption pkgs "radicle-node" { };
|
||||||
|
privateKeyFile = mkOption {
|
||||||
|
type = with types; either path str;
|
||||||
|
description = ''
|
||||||
|
SSH private key generated by `rad auth`.
|
||||||
|
|
||||||
|
If it contains a colon (`:`) the string before the colon
|
||||||
|
is taken as the credential name
|
||||||
|
and the string after as a path encrypted with `systemd-creds`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
publicKeyFile = mkOption {
|
||||||
|
type = with types; either path str;
|
||||||
|
description = ''
|
||||||
|
SSH public key generated by `rad auth`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
node = {
|
||||||
|
listenAddress = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "0.0.0.0";
|
||||||
|
example = "127.0.0.1";
|
||||||
|
description = "The IP address on which `radicle-node` listens.";
|
||||||
|
};
|
||||||
|
listenPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8776;
|
||||||
|
description = "The port on which `radicle-node` listens.";
|
||||||
|
};
|
||||||
|
openFirewall = mkEnableOption "opening the firewall for `radicle-node`";
|
||||||
|
extraArgs = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [ ];
|
||||||
|
description = "Extra arguments for `radicle-node`";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
configFile = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
internal = true;
|
||||||
|
default = (json.generate "config.json" cfg.settings).overrideAttrs (previousAttrs: {
|
||||||
|
preferLocalBuild = true;
|
||||||
|
# None of the usual phases are run here because runCommandWith uses buildCommand,
|
||||||
|
# so just append to buildCommand what would usually be a checkPhase.
|
||||||
|
buildCommand = previousAttrs.buildCommand + optionalString cfg.checkConfig ''
|
||||||
|
ln -s $out config.json
|
||||||
|
install -D -m 644 /dev/stdin keys/radicle.pub <<<"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBgFMhajUng+Rjj/sCFXI9PzG8BQjru2n7JgUVF1Kbv5 snakeoil"
|
||||||
|
export RAD_HOME=$PWD
|
||||||
|
${getExe' pkgs.buildPackages.radicle-node "rad"} config >/dev/null || {
|
||||||
|
cat -n config.json
|
||||||
|
echo "Invalid config.json according to rad."
|
||||||
|
echo "Please double-check your services.radicle.settings (producing the config.json above),"
|
||||||
|
echo "some settings may be missing or have the wrong type."
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
checkConfig = mkEnableOption "checking the {file}`config.json` file resulting from {option}`services.radicle.settings`" // { default = true; };
|
||||||
|
settings = mkOption {
|
||||||
|
description = ''
|
||||||
|
See https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/radicle/src/node/config.rs#L275
|
||||||
|
'';
|
||||||
|
default = { };
|
||||||
|
type = types.submodule {
|
||||||
|
freeformType = json.type;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
httpd = {
|
||||||
|
enable = mkEnableOption "Radicle HTTP gateway to radicle-node";
|
||||||
|
package = mkPackageOption pkgs "radicle-httpd" { };
|
||||||
|
listenAddress = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "The IP address on which `radicle-httpd` listens.";
|
||||||
|
};
|
||||||
|
listenPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8080;
|
||||||
|
description = "The port on which `radicle-httpd` listens.";
|
||||||
|
};
|
||||||
|
nginx = mkOption {
|
||||||
|
# Type of a single virtual host, or null.
|
||||||
|
type = types.nullOr (types.submodule (
|
||||||
|
recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {
|
||||||
|
options.serverName = {
|
||||||
|
default = "radicle-${config.networking.hostName}.${config.networking.domain}";
|
||||||
|
defaultText = "radicle-\${config.networking.hostName}.\${config.networking.domain}";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
));
|
||||||
|
default = null;
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
serverAliases = [
|
||||||
|
"seed.''${config.networking.domain}"
|
||||||
|
];
|
||||||
|
enableACME = false;
|
||||||
|
useACMEHost = config.networking.domain;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
With this option, you can customize an nginx virtual host which already has sensible defaults for `radicle-httpd`.
|
||||||
|
Set to `{}` if you do not need any customization to the virtual host.
|
||||||
|
If enabled, then by default, the {option}`serverName` is
|
||||||
|
`radicle-''${config.networking.hostName}.''${config.networking.domain}`,
|
||||||
|
TLS is active, and certificates are acquired via ACME.
|
||||||
|
If this is set to null (the default), no nginx virtual host will be configured.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
extraArgs = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [ ];
|
||||||
|
description = "Extra arguments for `radicle-httpd`";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable (mkMerge [
|
||||||
|
{
|
||||||
|
systemd.services.radicle-node = mkMerge [
|
||||||
|
(commonServiceConfig "radicle-node")
|
||||||
|
{
|
||||||
|
description = "Radicle Node";
|
||||||
|
documentation = [ "man:radicle-node(1)" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${getExe' cfg.package "radicle-node"} --force --listen ${cfg.node.listenAddress}:${toString cfg.node.listenPort} ${escapeShellArgs cfg.node.extraArgs}";
|
||||||
|
Restart = mkDefault "on-failure";
|
||||||
|
RestartSec = "30";
|
||||||
|
SocketBindAllow = [ "tcp:${toString cfg.node.listenPort}" ];
|
||||||
|
SystemCallFilter = mkAfter [
|
||||||
|
# Needed by git upload-pack which calls alarm() and setitimer() when providing a rad clone
|
||||||
|
"@timer"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
confinement.packages = [
|
||||||
|
cfg.package
|
||||||
|
];
|
||||||
|
}
|
||||||
|
# Give only access to the private key to radicle-node.
|
||||||
|
{
|
||||||
|
serviceConfig =
|
||||||
|
let keyCred = builtins.split ":" "${cfg.privateKeyFile}"; in
|
||||||
|
if length keyCred > 1
|
||||||
|
then {
|
||||||
|
LoadCredentialEncrypted = [ cfg.privateKeyFile ];
|
||||||
|
# Note that neither %d nor ${CREDENTIALS_DIRECTORY} works in BindReadOnlyPaths=
|
||||||
|
BindReadOnlyPaths = [ "/run/credentials/radicle-node.service/${head keyCred}:${env.RAD_HOME}/keys/radicle" ];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LoadCredential = [ "radicle:${cfg.privateKeyFile}" ];
|
||||||
|
BindReadOnlyPaths = [ "/run/credentials/radicle-node.service/radicle:${env.RAD_HOME}/keys/radicle" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.systemPackages = [
|
||||||
|
rad-system
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.firewall = mkIf cfg.node.openFirewall {
|
||||||
|
allowedTCPPorts = [ cfg.node.listenPort ];
|
||||||
|
};
|
||||||
|
|
||||||
|
users = {
|
||||||
|
users.radicle = {
|
||||||
|
description = "Radicle";
|
||||||
|
group = "radicle";
|
||||||
|
home = env.HOME;
|
||||||
|
isSystemUser = true;
|
||||||
|
};
|
||||||
|
groups.radicle = {
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
(mkIf cfg.httpd.enable (mkMerge [
|
||||||
|
{
|
||||||
|
systemd.services.radicle-httpd = mkMerge [
|
||||||
|
(commonServiceConfig "radicle-httpd")
|
||||||
|
{
|
||||||
|
description = "Radicle HTTP gateway to radicle-node";
|
||||||
|
documentation = [ "man:radicle-httpd(1)" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${getExe' cfg.httpd.package "radicle-httpd"} --listen ${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort} ${escapeShellArgs cfg.httpd.extraArgs}";
|
||||||
|
Restart = mkDefault "on-failure";
|
||||||
|
RestartSec = "10";
|
||||||
|
SocketBindAllow = [ "tcp:${toString cfg.httpd.listenPort}" ];
|
||||||
|
SystemCallFilter = mkAfter [
|
||||||
|
# Needed by git upload-pack which calls alarm() and setitimer() when providing a git clone
|
||||||
|
"@timer"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
confinement.packages = [
|
||||||
|
cfg.httpd.package
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
(mkIf (cfg.httpd.nginx != null) {
|
||||||
|
services.nginx.virtualHosts.${cfg.httpd.nginx.serverName} = lib.mkMerge [
|
||||||
|
cfg.httpd.nginx
|
||||||
|
{
|
||||||
|
forceSSL = mkDefault true;
|
||||||
|
enableACME = mkDefault true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort}";
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
services.radicle.settings = {
|
||||||
|
node.alias = mkDefault cfg.httpd.nginx.serverName;
|
||||||
|
node.externalAddresses = mkDefault [
|
||||||
|
"${cfg.httpd.nginx.serverName}:${toString cfg.node.listenPort}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
]);
|
||||||
|
|
||||||
|
meta.maintainers = with lib.maintainers; [
|
||||||
|
julm
|
||||||
|
lorenzleutgeb
|
||||||
|
];
|
||||||
|
}
|
@ -813,6 +813,7 @@ in {
|
|||||||
rabbitmq = handleTest ./rabbitmq.nix {};
|
rabbitmq = handleTest ./rabbitmq.nix {};
|
||||||
radarr = handleTest ./radarr.nix {};
|
radarr = handleTest ./radarr.nix {};
|
||||||
radicale = handleTest ./radicale.nix {};
|
radicale = handleTest ./radicale.nix {};
|
||||||
|
radicle = runTest ./radicle.nix;
|
||||||
ragnarwm = handleTest ./ragnarwm.nix {};
|
ragnarwm = handleTest ./ragnarwm.nix {};
|
||||||
rasdaemon = handleTest ./rasdaemon.nix {};
|
rasdaemon = handleTest ./rasdaemon.nix {};
|
||||||
readarr = handleTest ./readarr.nix {};
|
readarr = handleTest ./readarr.nix {};
|
||||||
|
207
nixos/tests/radicle.nix
Normal file
207
nixos/tests/radicle.nix
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
# This test runs the radicle-node and radicle-httpd services on a seed host,
|
||||||
|
# and verifies that an alice peer can host a repository on the seed,
|
||||||
|
# and that a bob peer can send alice a patch via the seed.
|
||||||
|
|
||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
# The Node ID depends on nodes.seed.services.radicle.privateKeyFile
|
||||||
|
seed-nid = "z6Mkg52RcwDrPKRzzHaYgBkHH3Gi5p4694fvPstVE9HTyMB6";
|
||||||
|
seed-ssh-keys = import ./ssh-keys.nix pkgs;
|
||||||
|
seed-tls-certs = import common/acme/server/snakeoil-certs.nix;
|
||||||
|
|
||||||
|
commonHostConfig = { nodes, config, pkgs, ... }: {
|
||||||
|
environment.systemPackages = [
|
||||||
|
config.services.radicle.package
|
||||||
|
pkgs.curl
|
||||||
|
pkgs.gitMinimal
|
||||||
|
pkgs.jq
|
||||||
|
];
|
||||||
|
environment.etc."gitconfig".text = ''
|
||||||
|
[init]
|
||||||
|
defaultBranch = main
|
||||||
|
[user]
|
||||||
|
email = root@${config.networking.hostName}
|
||||||
|
name = ${config.networking.hostName}
|
||||||
|
'';
|
||||||
|
networking = {
|
||||||
|
extraHosts = ''
|
||||||
|
${nodes.seed.networking.primaryIPAddress} ${nodes.seed.services.radicle.httpd.nginx.serverName}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
security.pki.certificateFiles = [
|
||||||
|
seed-tls-certs.ca.cert
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
radicleConfig = { nodes, ... }: alias:
|
||||||
|
pkgs.writeText "config.json" (builtins.toJSON {
|
||||||
|
preferredSeeds = [
|
||||||
|
"${seed-nid}@seed:${toString nodes.seed.services.radicle.node.listenPort}"
|
||||||
|
];
|
||||||
|
node = {
|
||||||
|
inherit alias;
|
||||||
|
relay = "never";
|
||||||
|
seedingPolicy = {
|
||||||
|
default = "block";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
name = "radicle";
|
||||||
|
|
||||||
|
meta = with pkgs.lib.maintainers; {
|
||||||
|
maintainers = [
|
||||||
|
julm
|
||||||
|
lorenzleutgeb
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes = {
|
||||||
|
seed = { pkgs, config, ... }: {
|
||||||
|
imports = [ commonHostConfig ];
|
||||||
|
|
||||||
|
services.radicle = {
|
||||||
|
enable = true;
|
||||||
|
privateKeyFile = seed-ssh-keys.snakeOilEd25519PrivateKey;
|
||||||
|
publicKeyFile = seed-ssh-keys.snakeOilEd25519PublicKey;
|
||||||
|
node = {
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
httpd = {
|
||||||
|
enable = true;
|
||||||
|
nginx = {
|
||||||
|
serverName = seed-tls-certs.domain;
|
||||||
|
addSSL = true;
|
||||||
|
sslCertificate = seed-tls-certs.${seed-tls-certs.domain}.cert;
|
||||||
|
sslCertificateKey = seed-tls-certs.${seed-tls-certs.domain}.key;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
settings = {
|
||||||
|
preferredSeeds = [];
|
||||||
|
node = {
|
||||||
|
relay = "always";
|
||||||
|
seedingPolicy = {
|
||||||
|
default = "allow";
|
||||||
|
scope = "all";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [ 443 ];
|
||||||
|
};
|
||||||
|
|
||||||
|
alice = {
|
||||||
|
imports = [ commonHostConfig ];
|
||||||
|
};
|
||||||
|
|
||||||
|
bob = {
|
||||||
|
imports = [ commonHostConfig ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = { nodes, ... }@args: ''
|
||||||
|
start_all()
|
||||||
|
|
||||||
|
with subtest("seed can run radicle-node"):
|
||||||
|
# The threshold and/or hardening may have to be changed with new features/checks
|
||||||
|
print(seed.succeed("systemd-analyze security radicle-node.service --threshold=10 --no-pager"))
|
||||||
|
seed.wait_for_unit("radicle-node.service")
|
||||||
|
seed.wait_for_open_port(${toString nodes.seed.services.radicle.node.listenPort})
|
||||||
|
|
||||||
|
with subtest("seed can run radicle-httpd"):
|
||||||
|
# The threshold and/or hardening may have to be changed with new features/checks
|
||||||
|
print(seed.succeed("systemd-analyze security radicle-httpd.service --threshold=10 --no-pager"))
|
||||||
|
seed.wait_for_unit("radicle-httpd.service")
|
||||||
|
seed.wait_for_open_port(${toString nodes.seed.services.radicle.httpd.listenPort})
|
||||||
|
seed.wait_for_open_port(443)
|
||||||
|
assert alice.succeed("curl -sS 'https://${nodes.seed.services.radicle.httpd.nginx.serverName}/api/v1' | jq -r .nid") == "${seed-nid}\n"
|
||||||
|
assert bob.succeed("curl -sS 'https://${nodes.seed.services.radicle.httpd.nginx.serverName}/api/v1' | jq -r .nid") == "${seed-nid}\n"
|
||||||
|
|
||||||
|
with subtest("alice can create a Node ID"):
|
||||||
|
alice.succeed("rad auth --alias alice --stdin </dev/null")
|
||||||
|
alice.copy_from_host("${radicleConfig args "alice"}", "/root/.radicle/config.json")
|
||||||
|
with subtest("alice can run a node"):
|
||||||
|
alice.succeed("rad node start")
|
||||||
|
with subtest("alice can create a Git repository"):
|
||||||
|
alice.succeed(
|
||||||
|
"mkdir /tmp/repo",
|
||||||
|
"git -C /tmp/repo init",
|
||||||
|
"echo hello world > /tmp/repo/testfile",
|
||||||
|
"git -C /tmp/repo add .",
|
||||||
|
"git -C /tmp/repo commit -m init"
|
||||||
|
)
|
||||||
|
with subtest("alice can create a Repository ID"):
|
||||||
|
alice.succeed(
|
||||||
|
"cd /tmp/repo && rad init --name repo --description descr --default-branch main --public"
|
||||||
|
)
|
||||||
|
alice_repo_rid=alice.succeed("cd /tmp/repo && rad inspect --rid").rstrip("\n")
|
||||||
|
with subtest("alice can send a repository to the seed"):
|
||||||
|
alice.succeed(f"rad sync --seed ${seed-nid} {alice_repo_rid}")
|
||||||
|
|
||||||
|
with subtest(f"seed can receive the repository {alice_repo_rid}"):
|
||||||
|
seed.wait_until_succeeds("test 1 = \"$(rad-system stats | jq .local.repos)\"")
|
||||||
|
|
||||||
|
with subtest("bob can create a Node ID"):
|
||||||
|
bob.succeed("rad auth --alias bob --stdin </dev/null")
|
||||||
|
bob.copy_from_host("${radicleConfig args "bob"}", "/root/.radicle/config.json")
|
||||||
|
bob.succeed("rad node start")
|
||||||
|
with subtest("bob can clone alice's repository from the seed"):
|
||||||
|
bob.succeed(f"rad clone {alice_repo_rid} /tmp/repo")
|
||||||
|
assert bob.succeed("cat /tmp/repo/testfile") == "hello world\n"
|
||||||
|
|
||||||
|
with subtest("bob can clone alice's repository from the seed through the HTTP gateway"):
|
||||||
|
bob.succeed(f"git clone https://${nodes.seed.services.radicle.httpd.nginx.serverName}/{alice_repo_rid[4:]}.git /tmp/repo-http")
|
||||||
|
assert bob.succeed("cat /tmp/repo-http/testfile") == "hello world\n"
|
||||||
|
|
||||||
|
with subtest("alice can push the main branch to the rad remote"):
|
||||||
|
alice.succeed(
|
||||||
|
"echo hello bob > /tmp/repo/testfile",
|
||||||
|
"git -C /tmp/repo add .",
|
||||||
|
"git -C /tmp/repo commit -m 'hello to bob'",
|
||||||
|
"git -C /tmp/repo push rad main"
|
||||||
|
)
|
||||||
|
with subtest("bob can sync bob's repository from the seed"):
|
||||||
|
bob.succeed(
|
||||||
|
"cd /tmp/repo && rad sync --seed ${seed-nid}",
|
||||||
|
"cd /tmp/repo && git pull"
|
||||||
|
)
|
||||||
|
assert bob.succeed("cat /tmp/repo/testfile") == "hello bob\n"
|
||||||
|
|
||||||
|
with subtest("bob can push a patch"):
|
||||||
|
bob.succeed(
|
||||||
|
"echo hello alice > /tmp/repo/testfile",
|
||||||
|
"git -C /tmp/repo checkout -b for-alice",
|
||||||
|
"git -C /tmp/repo add .",
|
||||||
|
"git -C /tmp/repo commit -m 'hello to alice'",
|
||||||
|
"git -C /tmp/repo push -o patch.message='hello for alice' rad HEAD:refs/patches"
|
||||||
|
)
|
||||||
|
|
||||||
|
bob_repo_patch1_pid=bob.succeed("cd /tmp/repo && git branch --remotes | sed -ne 's:^ *rad/patches/::'p").rstrip("\n")
|
||||||
|
with subtest("alice can receive the patch"):
|
||||||
|
alice.wait_until_succeeds("test 1 = \"$(rad stats | jq .local.patches)\"")
|
||||||
|
alice.succeed(
|
||||||
|
f"cd /tmp/repo && rad patch show {bob_repo_patch1_pid} | grep 'opened by bob'",
|
||||||
|
f"cd /tmp/repo && rad patch checkout {bob_repo_patch1_pid}"
|
||||||
|
)
|
||||||
|
assert alice.succeed("cat /tmp/repo/testfile") == "hello alice\n"
|
||||||
|
with subtest("alice can comment the patch"):
|
||||||
|
alice.succeed(
|
||||||
|
f"cd /tmp/repo && rad patch comment {bob_repo_patch1_pid} -m thank-you"
|
||||||
|
)
|
||||||
|
with subtest("alice can merge the patch"):
|
||||||
|
alice.succeed(
|
||||||
|
"git -C /tmp/repo checkout main",
|
||||||
|
f"git -C /tmp/repo merge patch/{bob_repo_patch1_pid[:7]}",
|
||||||
|
"git -C /tmp/repo push rad main",
|
||||||
|
"cd /tmp/repo && rad patch list | grep -qxF 'Nothing to show.'"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
}
|
@ -7,6 +7,8 @@
|
|||||||
, lib
|
, lib
|
||||||
, makeWrapper
|
, makeWrapper
|
||||||
, man-db
|
, man-db
|
||||||
|
, nixos
|
||||||
|
, nixosTests
|
||||||
, openssh
|
, openssh
|
||||||
, radicle-node
|
, radicle-node
|
||||||
, runCommand
|
, runCommand
|
||||||
@ -89,6 +91,19 @@
|
|||||||
|
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
nixos-build = lib.recurseIntoAttrs {
|
||||||
|
checkConfig-success = (nixos {
|
||||||
|
services.radicle.settings = {
|
||||||
|
node.alias = "foo";
|
||||||
|
};
|
||||||
|
}).config.services.radicle.configFile;
|
||||||
|
checkConfig-failure = testers.testBuildFailure (nixos {
|
||||||
|
services.radicle.settings = {
|
||||||
|
node.alias = null;
|
||||||
|
};
|
||||||
|
}).config.services.radicle.configFile;
|
||||||
|
};
|
||||||
|
nixos-run = nixosTests.radicle;
|
||||||
};
|
};
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
|
Loading…
Reference in New Issue
Block a user