uboot: (firmwareOdroidC2/C4) don't invoke patch tool, use patches = [] instead

https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh#L948
this can do it nicely.

Signed-off-by: Anton Arapov <anton@deadbeef.mx>
This commit is contained in:
Anton Arapov 2021-04-03 12:58:10 +02:00 committed by Alan Daniels
commit 56de2bcd43
30691 changed files with 3076956 additions and 0 deletions

View file

@ -0,0 +1,236 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
scfg = cfg.builds;
rcfg = config.services.redis;
iniKey = "builds.sr.ht";
drv = pkgs.sourcehut.buildsrht;
in
{
options.services.sourcehut.builds = {
user = mkOption {
type = types.str;
default = "buildsrht";
description = ''
User for builds.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5002;
description = ''
Port on which the "builds" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "builds.sr.ht";
description = ''
PostgreSQL database name for builds.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/buildsrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/buildsrht"'';
description = ''
State path for builds.sr.ht.
'';
};
enableWorker = mkOption {
type = types.bool;
default = false;
description = ''
Run workers for builds.sr.ht.
'';
};
images = mkOption {
type = types.attrsOf (types.attrsOf (types.attrsOf types.package));
default = { };
example = lib.literalExpression ''(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.
'';
};
};
config = with scfg; let
image_dirs = lib.lists.flatten (
lib.attrsets.mapAttrsToList
(distro: revs:
lib.attrsets.mapAttrsToList
(rev: archs:
lib.attrsets.mapAttrsToList
(arch: image:
pkgs.runCommand "buildsrht-images" { } ''
mkdir -p $out/${distro}/${rev}/${arch}
ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
'')
archs)
revs)
scfg.images);
image_dir_pre = pkgs.symlinkJoin {
name = "builds.sr.ht-worker-images-pre";
paths = image_dirs ++ [
"${pkgs.sourcehut.buildsrht}/lib/images"
];
};
image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
mkdir -p $out/images
cp -Lr ${image_dir_pre}/* $out/images
'';
in
lib.mkIf (cfg.enable && elem "builds" cfg.services) {
users = {
users = {
"${user}" = {
isSystemUser = true;
group = user;
extraGroups = lib.optionals cfg.builds.enableWorker [ "docker" ];
description = "builds.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services.postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
systemd = {
tmpfiles.rules = [
"d ${statePath} 0755 ${user} ${user} -"
] ++ (lib.optionals cfg.builds.enableWorker
[ "d ${statePath}/logs 0775 ${user} ${user} - -" ]
);
services = {
buildsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey
{
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "builds.sr.ht website service";
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
# Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6
} // { preStart = " "; };
buildsrht-worker = {
enable = scfg.enableWorker;
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
partOf = [ "buildsrht.service" ];
description = "builds.sr.ht worker service";
path = [ pkgs.openssh pkgs.docker ];
preStart = let qemuPackage = pkgs.qemu_kvm;
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 = {
Type = "simple";
User = user;
Group = "nginx";
Restart = "always";
};
serviceConfig.ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
};
};
};
services.sourcehut.settings = {
# URL builds.sr.ht is being served at (protocol://domain)
"builds.sr.ht".origin = mkDefault "http://builds.${cfg.originBase}";
# Address and port to bind the debug server to
"builds.sr.ht".debug-host = mkDefault "0.0.0.0";
"builds.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"builds.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"builds.sr.ht".migrate-on-upgrade = mkDefault "yes";
# builds.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"builds.sr.ht".oauth-client-id = mkDefault null;
"builds.sr.ht".oauth-client-secret = mkDefault null;
# The redis connection used for the celery worker
"builds.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/3";
# The shell used for ssh
"builds.sr.ht".shell = mkDefault "runner-shell";
# Register the builds.sr.ht dispatcher
"git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys"} = mkDefault "${user}:${user}";
# Location for build logs, images, and control command
} // lib.attrsets.optionalAttrs scfg.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 "${scfg.statePath}/logs";
"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.${cfg.originBase}" =
if scfg.enableWorker then {
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".root = "${scfg.statePath}";
} else { };
services.nginx.virtualHosts."builds.${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.buildsrht}/${pkgs.sourcehut.python.sitePackages}/buildsrht";
};
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,127 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
cfgIni = cfg.settings;
scfg = cfg.dispatch;
iniKey = "dispatch.sr.ht";
drv = pkgs.sourcehut.dispatchsrht;
in
{
options.services.sourcehut.dispatch = {
user = mkOption {
type = types.str;
default = "dispatchsrht";
description = ''
User for dispatch.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5005;
description = ''
Port on which the "dispatch" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "dispatch.sr.ht";
description = ''
PostgreSQL database name for dispatch.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/dispatchsrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/dispatchsrht"'';
description = ''
State path for dispatch.sr.ht.
'';
};
};
config = with scfg; lib.mkIf (cfg.enable && elem "dispatch" cfg.services) {
users = {
users = {
"${user}" = {
isSystemUser = true;
group = user;
description = "dispatch.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services.postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
systemd = {
tmpfiles.rules = [
"d ${statePath} 0750 ${user} ${user} -"
];
services.dispatchsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "dispatch.sr.ht website service";
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};
};
services.sourcehut.settings = {
# URL dispatch.sr.ht is being served at (protocol://domain)
"dispatch.sr.ht".origin = mkDefault "http://dispatch.${cfg.originBase}";
# Address and port to bind the debug server to
"dispatch.sr.ht".debug-host = mkDefault "0.0.0.0";
"dispatch.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"dispatch.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"dispatch.sr.ht".migrate-on-upgrade = mkDefault "yes";
# dispatch.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"dispatch.sr.ht".oauth-client-id = mkDefault null;
"dispatch.sr.ht".oauth-client-secret = mkDefault null;
# Github Integration
"dispatch.sr.ht::github".oauth-client-id = mkDefault null;
"dispatch.sr.ht::github".oauth-client-secret = mkDefault null;
# Gitlab Integration
"dispatch.sr.ht::gitlab".enabled = mkDefault null;
"dispatch.sr.ht::gitlab".canonical-upstream = mkDefault "gitlab.com";
"dispatch.sr.ht::gitlab".repo-cache = mkDefault "./repo-cache";
# "dispatch.sr.ht::gitlab"."gitlab.com" = mkDefault "GitLab:application id:secret";
};
services.nginx.virtualHosts."dispatch.${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.dispatchsrht}/${pkgs.sourcehut.python.sitePackages}/dispatchsrht";
};
};
}

View file

@ -0,0 +1,217 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
scfg = cfg.git;
iniKey = "git.sr.ht";
rcfg = config.services.redis;
drv = pkgs.sourcehut.gitsrht;
in
{
options.services.sourcehut.git = {
user = mkOption {
type = types.str;
visible = false;
internal = true;
readOnly = true;
default = "git";
description = ''
User for git.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5001;
description = ''
Port on which the "git" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "git.sr.ht";
description = ''
PostgreSQL database name for git.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/gitsrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/gitsrht"'';
description = ''
State path for git.sr.ht.
'';
};
package = mkOption {
type = types.package;
default = pkgs.git;
defaultText = literalExpression "pkgs.git";
example = literalExpression "pkgs.gitFull";
description = ''
Git package for git.sr.ht. This can help silence collisions.
'';
};
};
config = with scfg; lib.mkIf (cfg.enable && elem "git" cfg.services) {
# sshd refuses to run with `Unsafe AuthorizedKeysCommand ... bad ownership or modes for directory /nix/store`
environment.etc."ssh/gitsrht-dispatch" = {
mode = "0755";
text = ''
#! ${pkgs.stdenv.shell}
${cfg.python}/bin/gitsrht-dispatch "$@"
'';
};
# Needs this in the $PATH when sshing into the server
environment.systemPackages = [ cfg.git.package ];
users = {
users = {
"${user}" = {
isSystemUser = true;
group = user;
# 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...
shell = pkgs.bash;
description = "git.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services = {
cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/gitsrht-periodic" ];
fcgiwrap.enable = true;
openssh.authorizedKeysCommand = ''/etc/ssh/gitsrht-dispatch "%u" "%h" "%t" "%k"'';
openssh.authorizedKeysCommandUser = "root";
openssh.extraConfig = ''
PermitUserEnvironment SRHT_*
'';
postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
};
systemd = {
tmpfiles.rules = [
# /var/log is owned by root
"f /var/log/git-srht-shell 0644 ${user} ${user} -"
"d ${statePath} 0750 ${user} ${user} -"
"d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
];
services = {
gitsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "redis.service" "postgresql.service" "network.target" ];
requires = [ "redis.service" "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
# Needs internally to create repos at the very least
path = [ pkgs.git ];
description = "git.sr.ht website service";
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};
gitsrht-webhooks = {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "git.sr.ht webhooks service";
serviceConfig = {
Type = "simple";
User = user;
Restart = "always";
};
serviceConfig.ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
};
};
};
services.sourcehut.settings = {
# URL git.sr.ht is being served at (protocol://domain)
"git.sr.ht".origin = mkDefault "http://git.${cfg.originBase}";
# Address and port to bind the debug server to
"git.sr.ht".debug-host = mkDefault "0.0.0.0";
"git.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"git.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"git.sr.ht".migrate-on-upgrade = mkDefault "yes";
# The redis connection used for the webhooks worker
"git.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
# A post-update script which is installed in every git repo.
"git.sr.ht".post-update-script = mkDefault "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
# git.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"git.sr.ht".oauth-client-id = mkDefault null;
"git.sr.ht".oauth-client-secret = mkDefault null;
# Path to git repositories on disk
"git.sr.ht".repos = mkDefault "/var/lib/git";
"git.sr.ht".outgoing-domain = mkDefault "http://git.${cfg.originBase}";
# The authorized keys hook uses this to dispatch to various handlers
# The format is a program to exec into as the key, and the user to match as the
# value. When someone tries to log in as this user, this program is executed
# and is expected to omit an AuthorizedKeys file.
#
# Discard of the string context is in order to allow derivation-derived strings.
# This is safe if the relevant package is installed which will be the case if the setting is utilized.
"git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys"} = mkDefault "${user}:${user}";
};
services.nginx.virtualHosts."git.${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.gitsrht}/${pkgs.sourcehut.python.sitePackages}/gitsrht";
extraConfig = ''
location = /authorize {
proxy_pass http://${cfg.address}:${toString 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 /var/lib/git;
fastcgi_pass unix:/run/fcgiwrap.sock;
fastcgi_param SCRIPT_FILENAME ${pkgs.git}/bin/git-http-backend;
fastcgi_param PATH_INFO $uri;
fastcgi_param GIT_PROJECT_ROOT $document_root;
fastcgi_read_timeout 500s;
include ${config.services.nginx.package}/conf/fastcgi_params;
gzip off;
}
'';
};
};
}

View file

@ -0,0 +1,175 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
scfg = cfg.hg;
iniKey = "hg.sr.ht";
rcfg = config.services.redis;
drv = pkgs.sourcehut.hgsrht;
in
{
options.services.sourcehut.hg = {
user = mkOption {
type = types.str;
internal = true;
readOnly = true;
default = "hg";
description = ''
User for hg.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5010;
description = ''
Port on which the "hg" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "hg.sr.ht";
description = ''
PostgreSQL database name for hg.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/hgsrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/hgsrht"'';
description = ''
State path for hg.sr.ht.
'';
};
cloneBundles = mkOption {
type = types.bool;
default = false;
description = ''
Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
'';
};
};
config = with scfg; lib.mkIf (cfg.enable && elem "hg" cfg.services) {
# In case it ever comes into being
environment.etc."ssh/hgsrht-dispatch" = {
mode = "0755";
text = ''
#! ${pkgs.stdenv.shell}
${cfg.python}/bin/gitsrht-dispatch $@
'';
};
environment.systemPackages = [ pkgs.mercurial ];
users = {
users = {
"${user}" = {
isSystemUser = true;
group = user;
# Assuming hg.sr.ht needs this too
shell = pkgs.bash;
description = "hg.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services = {
cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/hgsrht-periodic" ]
++ optional cloneBundles "0 * * * * ${cfg.python}/bin/hgsrht-clonebundles";
openssh.authorizedKeysCommand = ''/etc/ssh/hgsrht-dispatch "%u" "%h" "%t" "%k"'';
openssh.authorizedKeysCommandUser = "root";
openssh.extraConfig = ''
PermitUserEnvironment SRHT_*
'';
postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
};
systemd = {
tmpfiles.rules = [
# /var/log is owned by root
"f /var/log/hg-srht-shell 0644 ${user} ${user} -"
"d ${statePath} 0750 ${user} ${user} -"
"d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
];
services.hgsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "redis.service" "postgresql.service" "network.target" ];
requires = [ "redis.service" "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.mercurial ];
description = "hg.sr.ht website service";
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};
};
services.sourcehut.settings = {
# URL hg.sr.ht is being served at (protocol://domain)
"hg.sr.ht".origin = mkDefault "http://hg.${cfg.originBase}";
# Address and port to bind the debug server to
"hg.sr.ht".debug-host = mkDefault "0.0.0.0";
"hg.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"hg.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# The redis connection used for the webhooks worker
"hg.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
# A post-update script which is installed in every mercurial repo.
"hg.sr.ht".changegroup-script = mkDefault "${cfg.python}/bin/hgsrht-hook-changegroup";
# hg.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"hg.sr.ht".oauth-client-id = mkDefault null;
"hg.sr.ht".oauth-client-secret = mkDefault null;
# Path to mercurial repositories on disk
"hg.sr.ht".repos = mkDefault "/var/lib/hg";
# Path to the srht mercurial extension
# (defaults to where the hgsrht code is)
# "hg.sr.ht".srhtext = mkDefault null;
# .hg/store size (in MB) past which the nightly job generates clone bundles.
# "hg.sr.ht".clone_bundle_threshold = mkDefault 50;
# Path to hg-ssh (if not in $PATH)
# "hg.sr.ht".hg_ssh = mkDefault /path/to/hg-ssh;
# The authorized keys hook uses this to dispatch to various handlers
# The format is a program to exec into as the key, and the user to match as the
# value. When someone tries to log in as this user, this program is executed
# and is expected to omit an AuthorizedKeys file.
#
# Uncomment the relevant lines to enable the various sr.ht dispatchers.
"hg.sr.ht::dispatch"."/run/current-system/sw/bin/hgsrht-keys" = mkDefault "${user}:${user}";
};
# TODO: requires testing and addition of hg-specific requirements
services.nginx.virtualHosts."hg.${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.hgsrht}/${pkgs.sourcehut.python.sitePackages}/hgsrht";
};
};
}

View file

@ -0,0 +1,120 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
cfgIni = cfg.settings;
scfg = cfg.hub;
iniKey = "hub.sr.ht";
drv = pkgs.sourcehut.hubsrht;
in
{
options.services.sourcehut.hub = {
user = mkOption {
type = types.str;
default = "hubsrht";
description = ''
User for hub.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5014;
description = ''
Port on which the "hub" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "hub.sr.ht";
description = ''
PostgreSQL database name for hub.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/hubsrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/hubsrht"'';
description = ''
State path for hub.sr.ht.
'';
};
};
config = with scfg; lib.mkIf (cfg.enable && elem "hub" cfg.services) {
users = {
users = {
"${user}" = {
isSystemUser = true;
group = user;
description = "hub.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services.postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
systemd = {
tmpfiles.rules = [
"d ${statePath} 0750 ${user} ${user} -"
];
services.hubsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "hub.sr.ht website service";
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};
};
services.sourcehut.settings = {
# URL hub.sr.ht is being served at (protocol://domain)
"hub.sr.ht".origin = mkDefault "http://hub.${cfg.originBase}";
# Address and port to bind the debug server to
"hub.sr.ht".debug-host = mkDefault "0.0.0.0";
"hub.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"hub.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"hub.sr.ht".migrate-on-upgrade = mkDefault "yes";
# hub.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"hub.sr.ht".oauth-client-id = mkDefault null;
"hub.sr.ht".oauth-client-secret = mkDefault null;
};
services.nginx.virtualHosts."${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.hubsrht}/${pkgs.sourcehut.python.sitePackages}/hubsrht";
};
services.nginx.virtualHosts."hub.${cfg.originBase}" = {
globalRedirect = "${cfg.originBase}";
forceSSL = true;
};
};
}

View file

@ -0,0 +1,187 @@
# Email setup is fairly involved, useful references:
# https://drewdevault.com/2018/08/05/Local-mail-server.html
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
cfgIni = cfg.settings;
scfg = cfg.lists;
iniKey = "lists.sr.ht";
rcfg = config.services.redis;
drv = pkgs.sourcehut.listssrht;
in
{
options.services.sourcehut.lists = {
user = mkOption {
type = types.str;
default = "listssrht";
description = ''
User for lists.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5006;
description = ''
Port on which the "lists" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "lists.sr.ht";
description = ''
PostgreSQL database name for lists.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/listssrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/listssrht"'';
description = ''
State path for lists.sr.ht.
'';
};
};
config = with scfg; lib.mkIf (cfg.enable && elem "lists" cfg.services) {
users = {
users = {
"${user}" = {
isSystemUser = true;
group = user;
extraGroups = [ "postfix" ];
description = "lists.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services.postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
systemd = {
tmpfiles.rules = [
"d ${statePath} 0750 ${user} ${user} -"
];
services = {
listssrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "lists.sr.ht website service";
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};
listssrht-process = {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "lists.sr.ht process service";
serviceConfig = {
Type = "simple";
User = user;
Restart = "always";
ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.process worker --loglevel=info";
};
};
listssrht-lmtp = {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "lists.sr.ht process service";
serviceConfig = {
Type = "simple";
User = user;
Restart = "always";
ExecStart = "${cfg.python}/bin/listssrht-lmtp";
};
};
listssrht-webhooks = {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "lists.sr.ht webhooks service";
serviceConfig = {
Type = "simple";
User = user;
Restart = "always";
ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
};
};
};
};
services.sourcehut.settings = {
# URL lists.sr.ht is being served at (protocol://domain)
"lists.sr.ht".origin = mkDefault "http://lists.${cfg.originBase}";
# Address and port to bind the debug server to
"lists.sr.ht".debug-host = mkDefault "0.0.0.0";
"lists.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"lists.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"lists.sr.ht".migrate-on-upgrade = mkDefault "yes";
# lists.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"lists.sr.ht".oauth-client-id = mkDefault null;
"lists.sr.ht".oauth-client-secret = mkDefault null;
# Outgoing email for notifications generated by users
"lists.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
# The redis connection used for the webhooks worker
"lists.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/2";
# The redis connection used for the celery worker
"lists.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/4";
# Network-key
"lists.sr.ht".network-key = mkDefault null;
# Allow creation
"lists.sr.ht".allow-new-lists = mkDefault "no";
# Posting Domain
"lists.sr.ht".posting-domain = mkDefault "lists.${cfg.originBase}";
# 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.
"lists.sr.ht::worker".sock = mkDefault "/tmp/lists.sr.ht-lmtp.sock";
# The lmtp daemon will make the unix socket group-read/write for users in this
# group.
"lists.sr.ht::worker".sock-group = mkDefault "postfix";
"lists.sr.ht::worker".reject-url = mkDefault "https://man.sr.ht/lists.sr.ht/etiquette.md";
"lists.sr.ht::worker".reject-mimetypes = mkDefault "text/html";
};
services.nginx.virtualHosts."lists.${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.listssrht}/${pkgs.sourcehut.python.sitePackages}/listssrht";
};
};
}

View file

@ -0,0 +1,124 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
cfgIni = cfg.settings;
scfg = cfg.man;
iniKey = "man.sr.ht";
drv = pkgs.sourcehut.mansrht;
in
{
options.services.sourcehut.man = {
user = mkOption {
type = types.str;
default = "mansrht";
description = ''
User for man.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5004;
description = ''
Port on which the "man" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "man.sr.ht";
description = ''
PostgreSQL database name for man.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/mansrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/mansrht"'';
description = ''
State path for man.sr.ht.
'';
};
};
config = with scfg; lib.mkIf (cfg.enable && elem "man" cfg.services) {
assertions =
[
{
assertion = hasAttrByPath [ "git.sr.ht" "oauth-client-id" ] cfgIni;
message = "man.sr.ht needs access to git.sr.ht.";
}
];
users = {
users = {
"${user}" = {
isSystemUser = true;
group = user;
description = "man.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services.postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
systemd = {
tmpfiles.rules = [
"d ${statePath} 0750 ${user} ${user} -"
];
services.mansrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "man.sr.ht website service";
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};
};
services.sourcehut.settings = {
# URL man.sr.ht is being served at (protocol://domain)
"man.sr.ht".origin = mkDefault "http://man.${cfg.originBase}";
# Address and port to bind the debug server to
"man.sr.ht".debug-host = mkDefault "0.0.0.0";
"man.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"man.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"man.sr.ht".migrate-on-upgrade = mkDefault "yes";
# man.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"man.sr.ht".oauth-client-id = mkDefault null;
"man.sr.ht".oauth-client-secret = mkDefault null;
};
services.nginx.virtualHosts."man.${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.mansrht}/${pkgs.sourcehut.python.sitePackages}/mansrht";
};
};
}

View file

@ -0,0 +1,213 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
cfgIni = cfg.settings;
scfg = cfg.meta;
iniKey = "meta.sr.ht";
rcfg = config.services.redis;
drv = pkgs.sourcehut.metasrht;
in
{
options.services.sourcehut.meta = {
user = mkOption {
type = types.str;
default = "metasrht";
description = ''
User for meta.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5000;
description = ''
Port on which the "meta" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "meta.sr.ht";
description = ''
PostgreSQL database name for meta.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/metasrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/metasrht"'';
description = ''
State path for meta.sr.ht.
'';
};
};
config = with scfg; lib.mkIf (cfg.enable && elem "meta" cfg.services) {
assertions =
[
{
assertion = with cfgIni."meta.sr.ht::billing"; enabled == "yes" -> (stripe-public-key != null && stripe-secret-key != null);
message = "If meta.sr.ht::billing is enabled, the keys should be defined.";
}
];
users = {
users = {
${user} = {
isSystemUser = true;
group = user;
description = "meta.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services.cron.systemCronJobs = [ "0 0 * * * ${cfg.python}/bin/metasrht-daily" ];
services.postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
systemd = {
tmpfiles.rules = [
"d ${statePath} 0750 ${user} ${user} -"
];
services = {
metasrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "meta.sr.ht website service";
preStart = ''
# Configure client(s) as "preauthorized"
${concatMapStringsSep "\n\n"
(attr: ''
if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
# Configure ${attr}'s OAuth client as "preauthorized"
psql ${database} \
-c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
fi
'')
(builtins.attrNames (filterAttrs
(k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
cfg.settings))}
'';
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};
metasrht-api = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "meta.sr.ht api service";
preStart = ''
# Configure client(s) as "preauthorized"
${concatMapStringsSep "\n\n"
(attr: ''
if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
# Configure ${attr}'s OAuth client as "preauthorized"
psql ${database} \
-c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
fi
'')
(builtins.attrNames (filterAttrs
(k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
cfg.settings))}
'';
serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b :${toString (port + 100)}";
};
metasrht-webhooks = {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "meta.sr.ht webhooks service";
serviceConfig = {
Type = "simple";
User = user;
Restart = "always";
ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
};
};
};
};
services.sourcehut.settings = {
# URL meta.sr.ht is being served at (protocol://domain)
"meta.sr.ht".origin = mkDefault "https://meta.${cfg.originBase}";
# Address and port to bind the debug server to
"meta.sr.ht".debug-host = mkDefault "0.0.0.0";
"meta.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"meta.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"meta.sr.ht".migrate-on-upgrade = mkDefault "yes";
# If "yes", the user will be sent the stock sourcehut welcome emails after
# signup (requires cron to be configured properly). These are specific to the
# sr.ht instance so you probably want to patch these before enabling this.
"meta.sr.ht".welcome-emails = mkDefault "no";
# The redis connection used for the webhooks worker
"meta.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/6";
# If "no", public registration will not be permitted.
"meta.sr.ht::settings".registration = mkDefault "no";
# Where to redirect new users upon registration
"meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${cfg.originBase}";
# How many invites each user is issued upon registration (only applicable if
# open registration is disabled)
"meta.sr.ht::settings".user-invites = mkDefault 5;
# Origin URL for API, 100 more than web
"meta.sr.ht".api-origin = mkDefault "http://localhost:5100";
# You can add aliases for the client IDs of commonly used OAuth clients here.
#
# Example:
"meta.sr.ht::aliases" = mkDefault { };
# "meta.sr.ht::aliases"."git.sr.ht" = 12345;
# "yes" to enable the billing system
"meta.sr.ht::billing".enabled = mkDefault "no";
# Get your keys at https://dashboard.stripe.com/account/apikeys
"meta.sr.ht::billing".stripe-public-key = mkDefault null;
"meta.sr.ht::billing".stripe-secret-key = mkDefault null;
};
services.nginx.virtualHosts."meta.${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.metasrht}/${pkgs.sourcehut.python.sitePackages}/metasrht";
};
};
}

View file

@ -0,0 +1,135 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
cfgIni = cfg.settings;
scfg = cfg.paste;
iniKey = "paste.sr.ht";
rcfg = config.services.redis;
drv = pkgs.sourcehut.pastesrht;
in
{
options.services.sourcehut.paste = {
user = mkOption {
type = types.str;
default = "pastesrht";
description = ''
User for paste.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5011;
description = ''
Port on which the "paste" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "paste.sr.ht";
description = ''
PostgreSQL database name for paste.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/pastesrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/pastesrht"'';
description = ''
State path for pastesrht.sr.ht.
'';
};
};
config = with scfg; lib.mkIf (cfg.enable && elem "paste" cfg.services) {
users = {
users = {
"${user}" = {
isSystemUser = true;
group = user;
description = "paste.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services.postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
systemd = {
tmpfiles.rules = [
"d ${statePath} 0750 ${user} ${user} -"
];
services = {
pastesrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "paste.sr.ht website service";
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};
pastesrht-webhooks = {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "paste.sr.ht webhooks service";
serviceConfig = {
Type = "simple";
User = user;
Restart = "always";
ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
};
};
};
};
services.sourcehut.settings = {
# URL paste.sr.ht is being served at (protocol://domain)
"paste.sr.ht".origin = mkDefault "http://paste.${cfg.originBase}";
# Address and port to bind the debug server to
"paste.sr.ht".debug-host = mkDefault "0.0.0.0";
"paste.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"paste.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"paste.sr.ht".migrate-on-upgrade = mkDefault "yes";
# paste.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"paste.sr.ht".oauth-client-id = mkDefault null;
"paste.sr.ht".oauth-client-secret = mkDefault null;
"paste.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/5";
};
services.nginx.virtualHosts."paste.${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.pastesrht}/${pkgs.sourcehut.python.sitePackages}/pastesrht";
};
};
}

View file

@ -0,0 +1,375 @@
srv:
{ configIniOfService
, srvsrht ? "${srv}srht" # Because "buildsrht" does not follow that pattern (missing an "s").
, iniKey ? "${srv}.sr.ht"
, webhooks ? false
, extraTimers ? {}
, mainService ? {}
, extraServices ? {}
, extraConfig ? {}
, port
}:
{ config, lib, pkgs, ... }:
with lib;
let
inherit (config.services) postgresql;
redis = config.services.redis.servers."sourcehut-${srvsrht}";
inherit (config.users) users;
cfg = config.services.sourcehut;
configIni = configIniOfService srv;
srvCfg = cfg.${srv};
baseService = serviceName: { allowStripe ? false }: extraService: let
runDir = "/run/sourcehut/${serviceName}";
rootDir = "/run/sourcehut/chroots/${serviceName}";
in
mkMerge [ extraService {
after = [ "network.target" ] ++
optional cfg.postgresql.enable "postgresql.service" ++
optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
requires =
optional cfg.postgresql.enable "postgresql.service" ++
optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
path = [ pkgs.gawk ];
environment.HOME = runDir;
serviceConfig = {
User = mkDefault srvCfg.user;
Group = mkDefault srvCfg.group;
RuntimeDirectory = [
"sourcehut/${serviceName}"
# Used by *srht-keys which reads ../config.ini
"sourcehut/${serviceName}/subdir"
"sourcehut/chroots/${serviceName}"
];
RuntimeDirectoryMode = "2750";
# No need for the chroot path once inside the chroot
InaccessiblePaths = [ "-+${rootDir}" ];
# g+rx is for group members (eg. fcgiwrap or nginx)
# to read Git/Mercurial repositories, buildlogs, etc.
# o+x is for intermediate directories created by BindPaths= and like,
# as they're owned by root:root.
UMask = "0026";
RootDirectory = rootDir;
RootDirectoryStartOnly = true;
PrivateTmp = true;
MountAPIVFS = true;
# config.ini is looked up in there, before /etc/srht/config.ini
# Note that it fails to be set in ExecStartPre=
WorkingDirectory = mkDefault ("-"+runDir);
BindReadOnlyPaths = [
builtins.storeDir
"/etc"
"/run/booted-system"
"/run/current-system"
"/run/systemd"
] ++
optional cfg.postgresql.enable "/run/postgresql" ++
optional cfg.redis.enable "/run/redis-sourcehut-${srvsrht}";
# LoadCredential= are unfortunately not available in ExecStartPre=
# Hence this one is run as root (the +) with RootDirectoryStartOnly=
# to reach credentials wherever they are.
# Note that each systemd service gets its own ${runDir}/config.ini file.
ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "${serviceName}-credentials" ''
set -x
# Replace values begining with a '<' by the content of the file whose name is after.
gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
${optionalString (!allowStripe) "gawk '!/^stripe-secret-key=/' |"}
install -o ${srvCfg.user} -g root -m 400 /dev/stdin ${runDir}/config.ini
'')];
# The following options are only for optimizing:
# systemd-analyze security
AmbientCapabilities = "";
CapabilityBoundingSet = "";
# ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateNetwork = mkDefault false;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
#SocketBindAllow = [ "tcp:${toString srvCfg.port}" "tcp:${toString srvCfg.prometheusPort}" ];
#SocketBindDeny = "any";
SystemCallFilter = [
"@system-service"
"~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@timer"
"@chown" "@setuid"
];
SystemCallArchitectures = "native";
};
} ];
in
{
options.services.sourcehut.${srv} = {
enable = mkEnableOption "${srv} service";
user = mkOption {
type = types.str;
default = srvsrht;
description = ''
User for ${srv}.sr.ht.
'';
};
group = mkOption {
type = types.str;
default = srvsrht;
description = ''
Group for ${srv}.sr.ht.
Membership grants access to the Git/Mercurial repositories by default,
but not to the config.ini file (where secrets are).
'';
};
port = mkOption {
type = types.port;
default = port;
description = ''
Port on which the "${srv}" backend should listen.
'';
};
redis = {
host = mkOption {
type = types.str;
default = "unix:/run/redis-sourcehut-${srvsrht}/redis.sock?db=0";
example = "redis://shared.wireguard:6379/0";
description = ''
The redis host URL. This is used for caching and temporary storage, and must
be shared between nodes (e.g. git1.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.
'';
};
};
postgresql = {
database = mkOption {
type = types.str;
default = "${srv}.sr.ht";
description = ''
PostgreSQL database name for the ${srv}.sr.ht service,
used if <xref linkend="opt-services.sourcehut.postgresql.enable"/> is <literal>true</literal>.
'';
};
};
gunicorn = {
extraArgs = mkOption {
type = with types; listOf str;
default = ["--timeout 120" "--workers 1" "--log-level=info"];
description = "Extra arguments passed to Gunicorn.";
};
};
} // optionalAttrs webhooks {
webhooks = {
extraArgs = mkOption {
type = with types; listOf str;
default = ["--loglevel DEBUG" "--pool eventlet" "--without-heartbeat"];
description = "Extra arguments passed to the Celery responsible for webhooks.";
};
celeryConfig = mkOption {
type = types.lines;
default = "";
description = "Content of the <literal>celeryconfig.py</literal> used by the Celery responsible for webhooks.";
};
};
};
config = lib.mkIf (cfg.enable && srvCfg.enable) (mkMerge [ extraConfig {
users = {
users = {
"${srvCfg.user}" = {
isSystemUser = true;
group = mkDefault srvCfg.group;
description = mkDefault "sourcehut user for ${srv}.sr.ht";
};
};
groups = {
"${srvCfg.group}" = { };
} // optionalAttrs (cfg.postgresql.enable
&& hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")) {
"postgres".members = [ srvCfg.user ];
} // optionalAttrs (cfg.redis.enable
&& hasSuffix "0" (redis.settings.unixsocketperm or "")) {
"redis-sourcehut-${srvsrht}".members = [ srvCfg.user ];
};
};
services.nginx = mkIf cfg.nginx.enable {
virtualHosts."${srv}.${cfg.settings."sr.ht".global-domain}" = mkMerge [ {
forceSSL = mkDefault true;
locations."/".proxyPass = "http://${cfg.listenAddress}:${toString srvCfg.port}";
locations."/static" = {
root = "${pkgs.sourcehut.${srvsrht}}/${pkgs.sourcehut.python.sitePackages}/${srvsrht}";
extraConfig = mkDefault ''
expires 30d;
'';
};
} cfg.nginx.virtualHost ];
};
services.postgresql = mkIf cfg.postgresql.enable {
authentication = ''
local ${srvCfg.postgresql.database} ${srvCfg.user} trust
'';
ensureDatabases = [ srvCfg.postgresql.database ];
ensureUsers = map (name: {
inherit name;
ensurePermissions = { "DATABASE \"${srvCfg.postgresql.database}\"" = "ALL PRIVILEGES"; };
}) [srvCfg.user];
};
services.sourcehut.services = mkDefault (filter (s: cfg.${s}.enable)
[ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
services.sourcehut.settings = mkMerge [
{
"${srv}.sr.ht".origin = mkDefault "https://${srv}.${cfg.settings."sr.ht".global-domain}";
}
(mkIf cfg.postgresql.enable {
"${srv}.sr.ht".connection-string = mkDefault "postgresql:///${srvCfg.postgresql.database}?user=${srvCfg.user}&host=/run/postgresql";
})
];
services.redis.servers."sourcehut-${srvsrht}" = mkIf cfg.redis.enable {
enable = true;
databases = 3;
syslog = true;
# TODO: set a more informed value
save = mkDefault [ [1800 10] [300 100] ];
settings = {
# TODO: set a more informed value
maxmemory = "128MB";
maxmemory-policy = "volatile-ttl";
};
};
systemd.services = mkMerge [
{
"${srvsrht}" = baseService srvsrht { allowStripe = srv == "meta"; } (mkMerge [
{
description = "sourcehut ${srv}.sr.ht website service";
before = optional cfg.nginx.enable "nginx.service";
wants = optional cfg.nginx.enable "nginx.service";
wantedBy = [ "multi-user.target" ];
path = optional cfg.postgresql.enable postgresql.package;
# Beware: change in credentials' content will not trigger restart.
restartTriggers = [ configIni ];
serviceConfig = {
Type = "simple";
Restart = mkDefault "always";
#RestartSec = mkDefault "2min";
StateDirectory = [ "sourcehut/${srvsrht}" ];
StateDirectoryMode = "2750";
ExecStart = "${cfg.python}/bin/gunicorn ${srvsrht}.app:app --name ${srvsrht} --bind ${cfg.listenAddress}:${toString srvCfg.port} " + concatStringsSep " " srvCfg.gunicorn.extraArgs;
};
preStart = let
version = pkgs.sourcehut.${srvsrht}.version;
stateDir = "/var/lib/sourcehut/${srvsrht}";
in mkBefore ''
set -x
# Use the /run/sourcehut/${srvsrht}/config.ini
# installed by a previous ExecStartPre= in baseService
cd /run/sourcehut/${srvsrht}
if test ! -e ${stateDir}/db; then
# Setup the initial database.
# Note that it stamps the alembic head afterward
${cfg.python}/bin/${srvsrht}-initdb
echo ${version} >${stateDir}/db
fi
${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
if [ "$(cat ${stateDir}/db)" != "${version}" ]; then
# Manage schema migrations using alembic
${cfg.python}/bin/${srvsrht}-migrate -a upgrade head
echo ${version} >${stateDir}/db
fi
''}
# Update copy of each users' profile to the latest
# See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
if test ! -e ${stateDir}/webhook; then
# Update ${iniKey}'s users' profile copy to the latest
${cfg.python}/bin/srht-update-profiles ${iniKey}
touch ${stateDir}/webhook
fi
'';
} mainService ]);
}
(mkIf webhooks {
"${srvsrht}-webhooks" = baseService "${srvsrht}-webhooks" {}
{
description = "sourcehut ${srv}.sr.ht webhooks service";
after = [ "${srvsrht}.service" ];
wantedBy = [ "${srvsrht}.service" ];
partOf = [ "${srvsrht}.service" ];
preStart = ''
cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" srvCfg.webhooks.celeryConfig} \
/run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
'';
serviceConfig = {
Type = "simple";
Restart = "always";
ExecStart = "${cfg.python}/bin/celery --app ${srvsrht}.webhooks worker --hostname ${srvsrht}-webhooks@%%h " + concatStringsSep " " srvCfg.webhooks.extraArgs;
# Avoid crashing: os.getloadavg()
ProcSubset = mkForce "all";
};
};
})
(mapAttrs (timerName: timer: (baseService timerName {} (mkMerge [
{
description = "sourcehut ${timerName} service";
after = [ "network.target" "${srvsrht}.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${cfg.python}/bin/${timerName}";
};
}
(timer.service or {})
]))) extraTimers)
(mapAttrs (serviceName: extraService: baseService serviceName {} (mkMerge [
{
description = "sourcehut ${serviceName} service";
# So that extraServices have the PostgreSQL database initialized.
after = [ "${srvsrht}.service" ];
wantedBy = [ "${srvsrht}.service" ];
partOf = [ "${srvsrht}.service" ];
serviceConfig = {
Type = "simple";
Restart = mkDefault "always";
};
}
extraService
])) extraServices)
];
systemd.timers = mapAttrs (timerName: timer:
{
description = "sourcehut timer for ${timerName}";
wantedBy = [ "timers.target" ];
inherit (timer) timerConfig;
}) extraTimers;
} ]);
}

View file

@ -0,0 +1,119 @@
<chapter xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xi="http://www.w3.org/2001/XInclude"
version="5.0"
xml:id="module-services-sourcehut">
<title>Sourcehut</title>
<para>
<link xlink:href="https://sr.ht.com/">Sourcehut</link> is an open-source,
self-hostable software development platform. The server setup can be automated using
<link linkend="opt-services.sourcehut.enable">services.sourcehut</link>.
</para>
<section xml:id="module-services-sourcehut-basic-usage">
<title>Basic usage</title>
<para>
Sourcehut is a Python and Go based set of applications.
This NixOS module also provides basic configuration integrating Sourcehut into locally running
<literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>,
<literal><link linkend="opt-services.redis.servers">services.redis.servers.sourcehut</link></literal>,
<literal><link linkend="opt-services.postfix.enable">services.postfix</link></literal>
and
<literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal> services.
</para>
<para>
A very basic configuration may look like this:
<programlisting>
{ pkgs, ... }:
let
fqdn =
let
join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
in join config.networking.hostName config.networking.domain;
in {
networking = {
<link linkend="opt-networking.hostName">hostName</link> = "srht";
<link linkend="opt-networking.domain">domain</link> = "tld";
<link linkend="opt-networking.firewall.allowedTCPPorts">firewall.allowedTCPPorts</link> = [ 22 80 443 ];
};
services.sourcehut = {
<link linkend="opt-services.sourcehut.enable">enable</link> = true;
<link linkend="opt-services.sourcehut.git.enable">git.enable</link> = true;
<link linkend="opt-services.sourcehut.man.enable">man.enable</link> = true;
<link linkend="opt-services.sourcehut.meta.enable">meta.enable</link> = true;
<link linkend="opt-services.sourcehut.nginx.enable">nginx.enable</link> = true;
<link linkend="opt-services.sourcehut.postfix.enable">postfix.enable</link> = true;
<link linkend="opt-services.sourcehut.postgresql.enable">postgresql.enable</link> = true;
<link linkend="opt-services.sourcehut.redis.enable">redis.enable</link> = true;
<link linkend="opt-services.sourcehut.settings">settings</link> = {
"sr.ht" = {
environment = "production";
global-domain = fqdn;
origin = "https://${fqdn}";
# Produce keys with srht-keygen from <package>sourcehut.coresrht</package>.
network-key = "/run/keys/path/to/network-key";
service-key = "/run/keys/path/to/service-key";
};
webhooks.private-key= "/run/keys/path/to/webhook-key";
};
};
<link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."${fqdn}".extraDomainNames</link> = [
"meta.${fqdn}"
"man.${fqdn}"
"git.${fqdn}"
];
services.nginx = {
<link linkend="opt-services.nginx.enable">enable</link> = true;
# only recommendedProxySettings are strictly required, but the rest make sense as well.
<link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
<link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
<link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
<link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
# Settings to setup what certificates are used for which endpoint.
<link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
<link linkend="opt-services.nginx.virtualHosts._name_.enableACME">"${fqdn}".enableACME</link> = true;
<link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"meta.${fqdn}".useACMEHost</link> = fqdn:
<link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"man.${fqdn}".useACMEHost</link> = fqdn:
<link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"git.${fqdn}".useACMEHost</link> = fqdn:
};
};
}
</programlisting>
</para>
<para>
The <literal>hostName</literal> option is used internally to configure the nginx
reverse-proxy. The <literal>settings</literal> attribute set is
used by the configuration generator and the result is placed in <literal>/etc/sr.ht/config.ini</literal>.
</para>
</section>
<section xml:id="module-services-sourcehut-configuration">
<title>Configuration</title>
<para>
All configuration parameters are also stored in
<literal>/etc/sr.ht/config.ini</literal> which is generated by
the module and linked from the store to ensure that all values from <literal>config.ini</literal>
can be modified by the module.
</para>
</section>
<section xml:id="module-services-sourcehut-httpd">
<title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
<para>
By default, <package>nginx</package> is used as reverse-proxy for <package>sourcehut</package>.
However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
<package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
<literal>settings</literal>.
</para>
</section>
</chapter>

View file

@ -0,0 +1,163 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.sourcehut;
opt = options.services.sourcehut;
cfgIni = cfg.settings;
scfg = cfg.todo;
iniKey = "todo.sr.ht";
rcfg = config.services.redis;
drv = pkgs.sourcehut.todosrht;
in
{
options.services.sourcehut.todo = {
user = mkOption {
type = types.str;
default = "todosrht";
description = ''
User for todo.sr.ht.
'';
};
port = mkOption {
type = types.port;
default = 5003;
description = ''
Port on which the "todo" module should listen.
'';
};
database = mkOption {
type = types.str;
default = "todo.sr.ht";
description = ''
PostgreSQL database name for todo.sr.ht.
'';
};
statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/todosrht";
defaultText = literalExpression ''"''${config.${opt.statePath}}/todosrht"'';
description = ''
State path for todo.sr.ht.
'';
};
};
config = with scfg; lib.mkIf (cfg.enable && elem "todo" cfg.services) {
users = {
users = {
"${user}" = {
isSystemUser = true;
group = user;
extraGroups = [ "postfix" ];
description = "todo.sr.ht user";
};
};
groups = {
"${user}" = { };
};
};
services.postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{
name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
}
];
};
systemd = {
tmpfiles.rules = [
"d ${statePath} 0750 ${user} ${user} -"
];
services = {
todosrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "todo.sr.ht website service";
serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};
todosrht-lmtp = {
after = [ "postgresql.service" "network.target" ];
bindsTo = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "todo.sr.ht process service";
serviceConfig = {
Type = "simple";
User = user;
Restart = "always";
ExecStart = "${cfg.python}/bin/todosrht-lmtp";
};
};
todosrht-webhooks = {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
description = "todo.sr.ht webhooks service";
serviceConfig = {
Type = "simple";
User = user;
Restart = "always";
ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
};
};
};
};
services.sourcehut.settings = {
# URL todo.sr.ht is being served at (protocol://domain)
"todo.sr.ht".origin = mkDefault "http://todo.${cfg.originBase}";
# Address and port to bind the debug server to
"todo.sr.ht".debug-host = mkDefault "0.0.0.0";
"todo.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"todo.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"todo.sr.ht".migrate-on-upgrade = mkDefault "yes";
# todo.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"todo.sr.ht".oauth-client-id = mkDefault null;
"todo.sr.ht".oauth-client-secret = mkDefault null;
# Outgoing email for notifications generated by users
"todo.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
# The redis connection used for the webhooks worker
"todo.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
# Network-key
"todo.sr.ht".network-key = mkDefault null;
# 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.
"todo.sr.ht::mail".sock = mkDefault "/tmp/todo.sr.ht-lmtp.sock";
# The lmtp daemon will make the unix socket group-read/write for users in this
# group.
"todo.sr.ht::mail".sock-group = mkDefault "postfix";
"todo.sr.ht::mail".posting-domain = mkDefault "todo.${cfg.originBase}";
};
services.nginx.virtualHosts."todo.${cfg.originBase}" = {
forceSSL = true;
locations."/".proxyPass = "http://${cfg.address}:${toString port}";
locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
locations."/static".root = "${pkgs.sourcehut.todosrht}/${pkgs.sourcehut.python.sitePackages}/todosrht";
};
};
}