{ config, pkgs, lib, ... }:
with lib;
let
inherit (config.services) postfix postgresql redis;
inherit (config.users) users groups;
cfg = config.services.sourcehut;
domain = cfg.settings."sr.ht".global-domain;
settingsFormat = pkgs.formats.ini {
listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
mkKeyValue = k: v:
if v == null then ""
else generators.mkKeyValueDefault {
mkValueString = v:
if v == true then "yes"
else if v == false then "no"
else generators.mkValueStringDefault {} v;
} "=" k v;
};
configIniOfService = srv: settingsFormat.generate "sourcehut-${srv}-config.ini"
# Each service needs access to only a subset of sections (and secrets).
(filterAttrs (k: v: v != null)
(mapAttrs (section: v:
let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" section; in
if srvMatch == null # Include sections shared by all services
|| head srvMatch == srv # Include sections for the service being configured
then v
# *srht-{dispatch,keys,shell,update-hook} share the same config.ini
else if srv == "ssh" && elem (head srvMatch) ["builds" "git" "hg"] && cfg.${head srvMatch}.enable then v
# Enable Web links and integrations between services.
else if tail srvMatch == [ null ] && elem (head srvMatch) cfg.services
then {
inherit (v) origin;
# mansrht crashes without it
oauth-client-id = v.oauth-client-id or null;
}
# Drop sub-sections of other services
else null)
(recursiveUpdate cfg.settings {
# Those paths are mounted using BindPaths= or BindReadOnlyPaths=
# for services needing access to them.
"git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
"hg.sr.ht".repos = "/var/lib/sourcehut/hgsrht/repos";
"git.sr.ht".post-update-script = "/var/lib/sourcehut/gitsrht/bin/post-update-script";
"hg.sr.ht".changegroup-script = "/var/lib/sourcehut/hgsrht/bin/changegroup-script";
})));
commonServiceSettings = srv: {
origin = mkOption {
description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
type = types.str;
default = "https://${srv}.localhost.localdomain";
};
debug-host = mkOption {
description = "Address to bind the debug server to.";
type = with types; nullOr str;
default = null;
};
debug-port = mkOption {
description = "Port to bind the debug server to.";
type = with types; nullOr str;
default = null;
};
connection-string = mkOption {
description = "SQLAlchemy connection string for the database.";
type = types.str;
default = "postgresql:///localhost?user=${srv}srht&host=/run/postgresql";
};
migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade" // { default = true; };
oauth-client-id = mkOption {
description = "${srv}.sr.ht's OAuth client id for meta.sr.ht.";
type = types.str;
};
oauth-client-secret = mkOption {
description = "${srv}.sr.ht's OAuth client secret for meta.sr.ht.";
type = types.path;
apply = s: "<" + toString s;
};
};
# Specialized python containing all the modules
python = pkgs.sourcehut.python.withPackages (ps: with ps; [
gunicorn
eventlet
# Sourcehut services
srht
buildsrht
dispatchsrht
gitsrht
hgsrht
hubsrht
listssrht
mansrht
metasrht
# Not a python package
#pagessrht
pastesrht
todosrht
]);
mkOptionNullOrStr = description: mkOption {
inherit description;
type = with types; nullOr str;
default = null;
};
in
{
options.services.sourcehut = {
enable = mkEnableOption ''
sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
task dispatching, wiki and account management services
'';
services = mkOption {
type = with types; listOf (enum
[ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
defaultText = "locally enabled services";
description = ''
Services that may be displayed as links in the title bar of the Web interface.
'';
};
listenAddress = mkOption {
type = types.str;
default = "localhost";
description = "Address to bind to.";
};
python = mkOption {
internal = true;
type = types.package;
default = python;
description = ''
The python package to use. It should contain references to the *srht modules and also
gunicorn.
'';
};
minio = {
enable = mkEnableOption ''local minio integration'';
};
nginx = {
enable = mkEnableOption ''local nginx integration'';
};
postfix = {
enable = mkEnableOption ''local postfix integration'';
};
postgresql = {
enable = mkEnableOption ''local postgresql integration'';
};
redis = {
enable = mkEnableOption ''local redis integration'';
firstDatabase = mkOption {
type = types.int;
default = 0;
description = ''
Number of the first Redis database to use.
At most 9 consecutive databases are currently used.
'';
};
};
settings = mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options."sr.ht" = {
global-domain = mkOption {
description = "Global domain name.";
type = types.str;
example = "example.com";
};
environment = mkOption {
description = "Values other than \"production\" adds a banner to each page.";
type = types.enum [ "development" "production" ];
default = "development";
};
network-key = mkOption {
description = ''
An absolute file path (which should be outside the Nix-store)
to a secret key to encrypt internal messages with. Use srht-keygen network
to
generate this key. It must be consistent between all services and nodes.
'';
type = types.path;
apply = s: "<" + toString s;
};
owner-email = mkOption {
description = "Owner's email.";
type = types.str;
default = "contact@example.com";
};
owner-name = mkOption {
description = "Owner's name.";
type = types.str;
default = "John Doe";
};
redis-host = mkOption {
type = with types; nullOr str;
description = ''
The redis host URL. This is used for caching and temporary storage, and must
be shared between nodes (e.g. g - 1it1.sr.ht and git2.sr.ht), but need not be
shared between services. It may be shared between services, however, with no
ill effect, if this better suits your infrastructure.
'';
};
site-blurb = mkOption {
description = "Blurb for your site.";
type = types.str;
default = "the hacker's forge";
};
site-info = mkOption {
description = "The top-level info page for your site.";
type = types.str;
default = "https://sourcehut.org";
};
service-key = mkOption {
description = ''
An absolute file path (which should be outside the Nix-store)
to a key used for encrypting session cookies. Use srht-keygen service
to
generate the service key. This must be shared between each node of the same
service (e.g. git1.sr.ht and git2.sr.ht), but different services may use
different keys. If you configure all of your services with the same
config.ini, you may use the same service-key for all of them.
'';
type = types.path;
apply = s: "<" + toString s;
};
site-name = mkOption {
description = "The name of your network of sr.ht-based sites.";
type = types.str;
default = "sourcehut";
};
source-url = mkOption {
description = "The source code for your fork of sr.ht.";
type = types.str;
default = "https://git.sr.ht/~sircmpwn/srht";
};
};
options.mail = {
smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
smtp-port = mkOption {
description = "Outgoing SMTP port.";
type = with types; nullOr port;
default = null;
};
smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
error-to = mkOptionNullOrStr "Address receiving application exceptions";
error-from = mkOptionNullOrStr "Address sending application exceptions";
pgp-privkey = mkOptionNullOrStr ''
An absolute file path (which should be outside the Nix-store)
to an OpenPGP private key.
Your PGP key information (DO NOT mix up pub and priv here)
You must remove the password from your secret key, if present.
You can do this with gpg --edit-key [key-id]
, then use the passwd
command and do not enter a new password.
'';
pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
};
options.objects = {
s3-upstream = mkOption {
description = "Configure the S3-compatible object storage service.";
type = with types; nullOr str;
default = null;
};
s3-access-key = mkOption {
description = "Access key to the S3-compatible object storage service";
type = with types; nullOr str;
default = null;
};
s3-secret-key = mkOption {
description = ''
An absolute file path (which should be outside the Nix-store)
to the secret key of the S3-compatible object storage service.
'';
type = with types; nullOr path;
default = null;
apply = mapNullable (s: "<" + toString s);
};
};
options.webhooks = {
private-key = mkOption {
description = ''
An absolute file path (which should be outside the Nix-store)
to a base64-encoded Ed25519 key for signing webhook payloads.
This should be consistent for all *.sr.ht sites,
as this key will be used to verify signatures
from other sites in your network.
Use the srht-keygen webhook
command to generate a key.
'';
type = types.path;
apply = s: "<" + toString s;
};
};
options."dispatch.sr.ht" = commonServiceSettings "dispatch" // {
};
options."dispatch.sr.ht::github" = {
oauth-client-id = mkOptionNullOrStr "OAuth client id.";
oauth-client-secret = mkOptionNullOrStr "OAuth client secret.";
};
options."dispatch.sr.ht::gitlab" = {
enabled = mkEnableOption "GitLab integration";
canonical-upstream = mkOption {
type = types.str;
description = "Canonical upstream.";
default = "gitlab.com";
};
repo-cache = mkOption {
type = types.str;
description = "Repository cache directory.";
default = "./repo-cache";
};
"gitlab.com" = mkOption {
type = with types; nullOr str;
description = "GitLab id and secret.";
default = null;
example = "GitLab:application id:secret";
};
};
options."builds.sr.ht" = commonServiceSettings "builds" // {
redis = mkOption {
description = "The redis connection used for the celery worker.";
type = types.str;
default = "redis://localhost:6379/3";
};
shell = mkOption {
description = "The shell used for ssh.";
type = types.str;
default = "runner-shell";
};
};
options."git.sr.ht" = commonServiceSettings "git" // {
outgoing-domain = mkOption {
description = "Outgoing domain.";
type = types.str;
default = "https://git.localhost.localdomain";
};
post-update-script = mkOption {
description = ''
A post-update script which is installed in every git repo.
This setting is propagated to newer and existing repositories.
'';
type = types.path;
default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
defaultText = "\${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
# Git hooks are run relative to their repository's directory,
# but gitsrht-update-hook looks up ../config.ini
apply = p: pkgs.writeShellScript "update-hook" ''
test -e "''${PWD%/*}"/config.ini ||
ln -s ${users."sshsrht".home}/../config.ini "''${PWD%/*}"/config.ini
exec -a "$0" '${p}' "$@"
'';
};
repos = mkOption {
description = ''
Path to git repositories on disk.
If changing the default, you must ensure that
the gitsrht's user as read and write access to it.
'';
type = types.str;
default = "/var/lib/sourcehut/gitsrht/repos";
};
webhooks = mkOption {
description = "The redis connection used for the webhooks worker.";
type = types.str;
default = "redis://localhost:6379/1";
};
};
options."hg.sr.ht" = commonServiceSettings "hg" // {
changegroup-script = mkOption {
description = ''
A changegroup script which is installed in every mercurial repo.
This setting is propagated to newer and existing repositories.
'';
type = types.str;
default = "${cfg.python}/bin/hgsrht-hook-changegroup";
};
repos = mkOption {
description = ''
Path to mercurial repositories on disk.
If changing the default, you must ensure that
the hgsrht's user as read and write access to it.
'';
type = types.str;
default = "/var/lib/sourcehut/hgsrht/repos";
};
srhtext = mkOptionNullOrStr ''
Path to the srht mercurial extension
(defaults to where the hgsrht code is)
'';
clone_bundle_threshold = mkOption {
description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
type = types.ints.unsigned;
default = 50;
};
hg_ssh = mkOption {
description = "Path to hg-ssh (if not in $PATH).";
type = types.str;
default = "${pkgs.mercurial}/bin/hg-ssh";
};
webhooks = mkOption {
description = "The redis connection used for the webhooks worker.";
type = types.str;
default = "redis://localhost:6379/8";
};
};
options."hub.sr.ht" = commonServiceSettings "hub" // {
};
options."lists.sr.ht" = commonServiceSettings "lists" // {
allow-new-lists = mkEnableOption "Allow creation of new lists.";
notify-from = mkOption {
description = "Outgoing email for notifications generated by users.";
type = types.str;
default = "lists-notify@localhost.localdomain";
};
posting-domain = mkOption {
description = "Posting domain.";
type = types.str;
default = "lists.localhost.localdomain";
};
redis = mkOption {
description = "The redis connection used for the celery worker.";
type = types.str;
default = "redis://localhost:6379/4";
};
webhooks = mkOption {
description = "The redis connection used for the webhooks worker.";
type = types.str;
default = "redis://localhost:6379/2";
};
};
options."lists.sr.ht::worker" = {
reject-mimetypes = mkOption {
type = with types; listOf str;
default = ["text/html"];
};
reject-url = mkOption {
description = "Reject URL.";
type = types.str;
default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
};
sock = mkOption {
description = ''
Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
Alternatively, specify IP:PORT and an SMTP server will be run instead.
'';
type = types.str;
default = "/tmp/lists.sr.ht-lmtp.sock";
};
sock-group = mkOption {
description = ''
The lmtp daemon will make the unix socket group-read/write
for users in this group.
'';
type = types.str;
default = "postfix";
};
};
options."man.sr.ht" = commonServiceSettings "man" // {
};
options."meta.sr.ht" =
removeAttrs (commonServiceSettings "meta")
["oauth-client-id" "oauth-client-secret"] // {
api-origin = mkOption {
description = "Origin URL for API, 100 more than web.";
type = types.str;
default = "http://localhost:5100";
};
webhooks = mkOption {
description = "The redis connection used for the webhooks worker.";
type = types.str;
default = "redis://localhost:6379/6";
};
welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
};
options."meta.sr.ht::settings" = {
registration = mkEnableOption "public registration";
onboarding-redirect = mkOption {
description = "Where to redirect new users upon registration.";
type = types.str;
default = "https://meta.localhost.localdomain";
};
user-invites = mkOption {
description = ''
How many invites each user is issued upon registration
(only applicable if open registration is disabled).
'';
type = types.ints.unsigned;
default = 5;
};
};
options."meta.sr.ht::aliases" = mkOption {
description = "Aliases for the client IDs of commonly used OAuth clients.";
type = with types; attrsOf int;
default = {};
example = { "git.sr.ht" = 12345; };
};
options."meta.sr.ht::billing" = {
enabled = mkEnableOption "the billing system";
stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
stripe-secret-key = mkOptionNullOrStr ''
An absolute file path (which should be outside the Nix-store)
to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
'' // {
apply = mapNullable (s: "<" + toString s);
};
};
options."pages.sr.ht" = commonServiceSettings "pages" // {
gemini-certs = mkOption {
description = ''
An absolute file path (which should be outside the Nix-store)
to Gemini certificates.
'';
type = with types; nullOr path;
default = null;
};
max-site-size = mkOption {
description = "Maximum size of any given site (post-gunzip), in MiB.";
type = types.int;
default = 1024;
};
user-domain = mkOption {
description = ''
Configures the user domain, if enabled.
All users are given .this.domain.
'';
type = with types; nullOr str;
default = null;
};
};
options."pages.sr.ht::api" = {
};
options."paste.sr.ht" = commonServiceSettings "paste" // {
webhooks = mkOption {
type = types.str;
default = "redis://localhost:6379/5";
};
};
options."todo.sr.ht" = commonServiceSettings "todo" // {
notify-from = mkOption {
description = "Outgoing email for notifications generated by users.";
type = types.str;
default = "todo-notify@localhost.localdomain";
};
webhooks = mkOption {
description = "The redis connection used for the webhooks worker.";
type = types.str;
default = "redis://localhost:6379/7";
};
};
options."todo.sr.ht::mail" = {
posting-domain = mkOption {
description = "Posting domain.";
type = types.str;
default = "todo.localhost.localdomain";
};
sock = mkOption {
description = ''
Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
Alternatively, specify IP:PORT and an SMTP server will be run instead.
'';
type = types.str;
default = "/tmp/todo.sr.ht-lmtp.sock";
};
sock-group = mkOption {
description = ''
The lmtp daemon will make the unix socket group-read/write
for users in this group.
'';
type = types.str;
default = "postfix";
};
};
};
default = { };
description = ''
The configuration for the sourcehut network.
'';
};
builds = {
enableWorker = mkEnableOption "worker for builds.sr.ht";
images = mkOption {
type = with types; attrsOf (attrsOf (attrsOf package));
default = { };
example = lib.literalExample ''(let
# Pinning unstable to allow usage with flakes and limit rebuilds.
pkgs_unstable = builtins.fetchGit {
url = "https://github.com/NixOS/nixpkgs";
rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
ref = "nixos-unstable";
};
image_from_nixpkgs = pkgs_unstable: (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
pkgs = (import pkgs_unstable {});
});
in
{
nixos.unstable.x86_64 = image_from_nixpkgs pkgs_unstable;
}
)'';
description = ''
Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
'';
};
};
git = {
package = mkOption {
type = types.package;
default = pkgs.git;
example = literalExample "pkgs.gitFull";
description = ''
Git package for git.sr.ht. This can help silence collisions.
'';
};
};
hg = {
package = mkOption {
type = types.package;
default = pkgs.mercurial;
description = ''
Mercurial package for hg.sr.ht. This can help silence collisions.
'';
};
cloneBundles = mkOption {
type = types.bool;
default = false;
description = ''
Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
'';
};
};
};
config = mkIf cfg.enable (mkMerge [
{
environment.systemPackages = [ pkgs.sourcehut.coresrht ];
services.sourcehut.settings = {
"git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
"lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
"lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
"meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
"todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
"todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
};
}
(mkIf cfg.postgresql.enable {
services.postgresql.enable = true;
})
(mkIf cfg.postfix.enable {
services.postfix.enable = true;
# Needed for sharing the LMTP sockets with JoinsNamespaceOf=
systemd.services.postfix.serviceConfig.PrivateTmp = true;
})
(mkIf cfg.redis.enable {
services.redis.enable = true;
services.sourcehut.settings."sr.ht".redis-host = mkDefault ("redis://localhost:6379/" + toString cfg.redis.firstDatabase);
})
(mkIf cfg.nginx.enable {
services.nginx.enable = true;
})
(mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
services.openssh = {
# Note that sshd will continue to honor AuthorizedKeysFile
authorizedKeysCommand = ''/etc/ssh/srht-dispatch "%u" "%h" "%t" "%k"'';
# The sshsrht-dispatch user needs:
# 1. to read ${users."sshsrht".home}/../config.ini,
# 2. to access the redis server in redis-host,
# 3. to access the postgresql server in the service's connection-string,
# 4. to query metasrht-api (through the HTTP API).
# Note that *srht-{dispatch,keys,shell,update-hook} will likely fail
# to write their log on /var/log with that user, and will fallback to stderr,
# making their log visible in sshd's log when sshd is in debug mode (-d).
# Alternatively, you can touch and chown sshsrht /var/log/gitsrht-{dispatch,keys,shell,update-hook}
# during your debug.
authorizedKeysCommandUser = users."sshsrht".name;
extraConfig = ''
PermitUserEnvironment SRHT_*
'';
};
environment.etc."ssh/srht-dispatch" = {
# sshd_config(5): The program must be owned by root, not writable by group or others
mode = "0755";
source = pkgs.writeShellScript "srht-dispatch" ''
set -e
cd ${users."sshsrht".home}
exec ${cfg.python}/bin/gitsrht-dispatch "$@"
'';
};
systemd.services.sshd = let configIni = configIniOfService "ssh"; in {
#path = optional cfg.git.enable [ cfg.git.package ];
restartTriggers = [ configIni ];
serviceConfig = {
RuntimeDirectory = [ "sourcehut/sshsrht/subdir" ];
BindReadOnlyPaths =
# Note that the path /usr/bin/*srht-* are hardcoded in multiple places in *.sr.ht,
# for instance to get the user from the [*.sr.ht::dispatch] settings.
optionals cfg.builds.enable [
"${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys:/usr/bin/buildsrht-keys"
"${pkgs.sourcehut.buildsrht}/bin/buildsrht-shell:/usr/bin/buildsrht-shell"
] ++
optionals cfg.git.enable [
"${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys:/usr/bin/gitsrht-keys"
"${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell:/usr/bin/gitsrht-shell"
] ++
optionals cfg.hg.enable [
"${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys:/usr/bin/htsrht-keys"
"${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell:/usr/bin/htsrht-shell"
];
ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "sshsrht-credentials" ''
# Replace values begining with a '<' by the content of the file whose name is after.
${pkgs.gawk}/bin/gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
install -o ${users."sshsrht".name} -g ${groups."sshsrht".name} -m 440 \
/dev/stdin ${users."sshsrht".home}/../config.ini
'')];
};
};
users = {
users."sshsrht" = {
isSystemUser = true;
# srht-dispatch, *srht-keys, and *srht-shell
# look up in ../config.ini from this directory;
# that config.ini being set in *srht.service's ExecStartPre=
home = "/run/sourcehut/sshsrht/subdir";
group =
# Unfortunately, AuthorizedKeysCommandUser does not honor supplementary groups,
# hence the main group is used.
if cfg.postgresql.enable
&& hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")
then groups.postgres.name
else groups.nogroup.name;
description = "sourcehut user for AuthorizedKeysCommand";
};
groups."sshsrht" = {};
};
})
]);
imports = [
(import ./service.nix "builds" {
inherit configIniOfService;
port = 5002;
redisDatabase = 3;
extraServices.buildsrht-worker = {
enable = cfg.builds.enableWorker;
partOf = [ "buildsrht.service" ];
path = [ pkgs.openssh pkgs.docker ];
preStart = let
qemuPackage = pkgs.qemu_kvm;
statePath = "/var/lib/sourcehut/buildsrht";
in ''
if [[ "$(docker images -q qemu:latest 2> /dev/null)" == "" || "$(cat ${statePath}/docker-image-qemu 2> /dev/null || true)" != "${qemuPackage.version}" ]]; then
# Create and import qemu:latest image for docker
${pkgs.dockerTools.streamLayeredImage {
name = "qemu";
tag = "latest";
contents = [ qemuPackage ];
}} | docker load
# Mark down current package version
printf "%s" "${qemuPackage.version}" > ${statePath}/docker-image-qemu
fi
'';
serviceConfig = {
ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
Group = mkIf cfg.nginx.enable "nginx";
};
};
extraConfig = let
image_dirs = flatten (
mapAttrsToList (distro: revs:
mapAttrsToList (rev: archs:
mapAttrsToList (arch: image:
pkgs.runCommandNoCC "buildsrht-images" { } ''
mkdir -p $out/${distro}/${rev}/${arch}
ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
''
) archs
) revs
) cfg.builds.images
);
image_dir_pre = pkgs.symlinkJoin {
name = "builds.sr.ht-worker-images-pre";
paths = image_dirs ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
};
image_dir = pkgs.runCommandNoCC "builds.sr.ht-worker-images" { } ''
mkdir -p $out/images
cp -Lr ${image_dir_pre}/* $out/images
'';
in {
users.users.${cfg.builds.user} = {
shell = pkgs.bash;
extraGroups = [ groups."sshsrht".name ];
};
users.groups.docker.members = mkIf cfg.builds.enableWorker [ cfg.builds.user ];
virtualisation.docker.enable = true;
# Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6
# FIXME: see if there is a better way than disabling preStart.
systemd.services.buildsrht.preStart = mkForce "";
services.sourcehut.settings = mkMerge [
{ # Register the builds.sr.ht dispatcher
"git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
mkDefault "${cfg.builds.user}:${cfg.builds.user}";
}
(mkIf cfg.builds.enableWorker {
# Default worker stores logs that are accessible via this address:port
"builds.sr.ht::worker".name = mkDefault "127.0.0.1:5020";
"builds.sr.ht::worker".buildlogs = mkDefault "/var/log/sourcehut/buildsrht";
"builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
"builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
"builds.sr.ht::worker".timeout = mkDefault "3m";
})
];
services.nginx.virtualHosts."logs.${domain}" = mkIf (cfg.nginx.enable && cfg.builds.enableWorker) {
listen = with builtins;
let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
[{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
locations."/logs".alias = cfg.settings."builds.sr.ht::worker".buildlogs + "/";
};
};
})
(import ./service.nix "dispatch" {
inherit configIniOfService;
port = 5005;
})
(import ./service.nix "git" (let
commonService = {
path = [ cfg.git.package ];
serviceConfig.BindPaths = [
"${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos"
];
serviceConfig.BindReadOnlyPaths = [
"${cfg.settings."git.sr.ht".post-update-script}:/var/lib/sourcehut/gitsrht/bin/post-update-script"
];
}; in {
inherit configIniOfService;
commonService = mkMerge [ commonService {
serviceConfig.StateDirectory = [ "sourcehut/gitsrht/repos" ];
} ];
port = 5001;
webhooks.redisDatabase = 1;
extraTimers.gitsrht-periodic = {
OnCalendar = ["20min"];
};
extraConfig = {
# https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
# Probably could use gitsrht-shell if output is restricted to just parameters...
users.users.${cfg.git.user} = {
shell = pkgs.bash;
# Allow reading of ${users."sshsrht".home}/../config.ini
extraGroups = [ groups."sshsrht".name ];
home = users.sshsrht.home;
};
services.sourcehut.settings = {
# Register the git.sr.ht dispatcher
"git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
mkDefault "${cfg.git.user}:${cfg.git.user}";
};
services.fcgiwrap.enable = mkIf cfg.nginx.enable true;
services.nginx.virtualHosts."git.${domain}" = mkIf cfg.nginx.enable {
extraConfig = ''
location = /authorize {
proxy_pass http://${cfg.listenAddress}:${toString cfg.git.port};
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ {
auth_request /authorize;
root ${cfg.settings."git.sr.ht".repos};
fastcgi_pass unix:/run/fcgiwrap.sock;
fastcgi_param SCRIPT_FILENAME ${cfg.git.package}/bin/git-http-backend;
fastcgi_param PATH_INFO $uri;
fastcgi_param GIT_PROJECT_ROOT $document_root;
fastcgi_param GIT_HTTP_EXPORT_ALL "";
fastcgi_read_timeout 500s;
include ${config.services.nginx.package}/conf/fastcgi_params;
gzip off;
}
'';
};
systemd.services.nginx = mkIf cfg.nginx.enable {
serviceConfig.BindReadOnlyPaths = [ cfg.settings."git.sr.ht".repos ];
};
systemd.services.sshd = commonService;
};
}))
(import ./service.nix "hg" (let
commonService = {
path = [ cfg.hg.package ];
serviceConfig.BindPaths = [
"${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos"
];
serviceConfig.BindReadOnlyPaths = [
"${cfg.settings."ht.sr.ht".changegroup-script}:/var/lib/sourcehut/hgsrht/bin/changegroup-script"
];
}; in {
inherit configIniOfService;
commonService = mkMerge [ commonService {
serviceConfig.StateDirectory = [ "sourcehut/hgsrht/repos" ];
} ];
port = 5010;
webhooks.redisDatabase = 8;
extraTimers.hgsrht-periodic = {
OnCalendar = ["20min"];
};
extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
OnCalendar = ["daily"];
AccuracySec = "1h";
};
extraConfig = {
users.users.${cfg.hg.user} = {
shell = pkgs.bash;
extraGroups = [ groups."sshsrht".name ];
};
services.sourcehut.settings = {
# Register the hg.sr.ht dispatcher
"hg.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
mkDefault "${cfg.hg.user}:${cfg.hg.user}";
};
systemd.services.sshd = commonService;
};
}))
(import ./service.nix "hub" {
inherit configIniOfService;
port = 5014;
extraConfig = {
services.nginx.virtualHosts."hub.${domain}" = mkIf cfg.nginx.enable {
serverAliases = [ domain ];
};
};
})
(import ./service.nix "lists" {
inherit configIniOfService;
port = 5006;
redisDatabase = 4;
webhooks.redisDatabase = 2;
extraServices.listssrht-lmtp = {
requires = [ "postfix.service" ];
unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
# Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
serviceConfig.PrivateUsers = mkForce false;
};
extraServices.listssrht-process = {
serviceConfig.ExecStart = "${cfg.python}/bin/celery -A listssrht.process worker --loglevel INFO --pool eventlet";
# Avoid crashing: os.getloadavg()
serviceConfig.ProcSubset = mkForce "all";
};
extraConfig = mkIf cfg.postfix.enable {
users.groups.${postfix.group}.members = [ cfg.lists.user ];
services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
services.postfix.transport = ''
lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
'';
};
})
(import ./service.nix "man" {
inherit configIniOfService;
port = 5004;
})
(import ./service.nix "meta" {
inherit configIniOfService;
port = 5000;
webhooks.redisDatabase = 6;
extraServices.metasrht-api = {
serviceConfig.Restart = "always";
serviceConfig.RestartSec = "2s";
preStart = concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
srv = head srvMatch;
oauthPath = "/var/lib/sourcehut/metasrht/${srv}.oauth";
in
# Configure client(s) as "preauthorized"
optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
if test ! -e "${oauthPath}" || [ "$(cat ${oauthPath})" != "${s.oauth-client-id}" ]; then
# Configure ${srv}'s OAuth client as "preauthorized"
psql '${cfg.meta.database}' \
-c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
printf "%s" "${s.oauth-client-id}" > "${oauthPath}"
fi
''
) cfg.settings));
path = [ config.services.postgresql.package ];
serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
};
extraTimers.metasrht-daily = {
OnCalendar = ["daily"];
AccuracySec = "1h";
};
extraConfig = {
assertions = [
{ assertion = let s = cfg.settings."meta.sr.ht::billing"; in
s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
}
];
environment.systemPackages = [
(pkgs.writeShellScriptBin "metasrht-manageuser" ''
set -eux
test "$(${pkgs.coreutils}/bin/id -n -u)" = '${cfg.meta.user}' ||
sudo -u '${cfg.meta.user}' "$0" "$@"
# In order to load config.ini
cd /run/sourcehut/metasrht ||
cat < ${stateDir}/db
fi
${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
# Just try all the migrations because they're not linked to the version
for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
done
''}
# Disable webhook
touch ${stateDir}/webhook
'';
serviceConfig = {
ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
};
};
})
(import ./service.nix "paste" {
inherit configIniOfService;
port = 5011;
webhooks.redisDatabase = 5;
})
(import ./service.nix "todo" {
inherit configIniOfService;
port = 5003;
webhooks.redisDatabase = 7;
extraServices.todosrht-lmtp = {
requires = [ "postfix.service" ];
unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
# Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
serviceConfig.PrivateUsers = mkForce false;
};
extraConfig = mkIf cfg.postfix.enable {
users.groups.${postfix.group}.members = [ cfg.todo.user ];
services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
services.postfix.transport = ''
todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
'';
};
})
(mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
[ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
(mkRenamedOptionModule [ "services" "sourcehut" "address" ]
[ "services" "sourcehut" "listenAddress" ])
];
meta.doc = ./sourcehut.xml;
meta.maintainers = with maintainers; [ julm tomberek ];
}