nixos/sourcehut: full rewrite, with fixes and hardening

Changed files
+1655 -194
nixos
modules
services
+1279 -131
nixos/modules/services/misc/sourcehut/default.nix
···
{ config, pkgs, lib, ... }:
-
with lib;
let
+
inherit (config.services) nginx postfix postgresql redis;
+
inherit (config.users) users groups;
cfg = config.services.sourcehut;
-
cfgIni = cfg.settings;
-
settingsFormat = pkgs.formats.ini { };
+
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
+
# 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.
+
"builds.sr.ht::worker".buildlogs = "/var/log/sourcehut/buildsrht/logs";
+
"git.sr.ht".post-update-script = "/var/lib/sourcehut/gitsrht/bin/post-update-script";
+
"git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
+
"hg.sr.ht".changegroup-script = "/var/lib/sourcehut/hgsrht/bin/changegroup-script";
+
"hg.sr.ht".repos = "/var/lib/sourcehut/hgsrht/repos";
+
# Making this a per service option despite being in a global section,
+
# so that it uses the redis-server used by the service.
+
"sr.ht".redis-host = cfg.${srv}.redis.host;
+
})));
+
commonServiceSettings = srv: {
+
origin = mkOption {
+
description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
+
type = types.str;
+
default = "https://${srv}.${domain}";
+
defaultText = "https://${srv}.example.com";
+
};
+
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
+
# For monitoring Celery: sudo -u listssrht celery --app listssrht.process -b redis+socket:///run/redis-sourcehut/redis.sock?virtual_host=5 flower
+
flower
# Sourcehut services
srht
buildsrht
···
listssrht
mansrht
metasrht
+
# Not a python package
+
#pagessrht
pastesrht
todosrht
]);
+
mkOptionNullOrStr = description: mkOption {
+
inherit description;
+
type = with types; nullOr str;
+
default = null;
+
};
in
{
-
imports =
-
[
-
./git.nix
-
./hg.nix
-
./hub.nix
-
./todo.nix
-
./man.nix
-
./meta.nix
-
./paste.nix
-
./builds.nix
-
./lists.nix
-
./dispatch.nix
-
(mkRemovedOptionModule [ "services" "sourcehut" "nginx" "enable" ] ''
-
The sourcehut module supports `nginx` as a local reverse-proxy by default and doesn't
-
support other reverse-proxies officially.
-
-
However it's possible to use an alternative reverse-proxy by
-
-
* disabling nginx
-
* adjusting the relevant settings for server addresses and ports directly
-
-
Further details about this can be found in the `Sourcehut`-section of the NixOS-manual.
-
'')
-
];
-
options.services.sourcehut = {
-
enable = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Enable sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
-
task dispatching, wiki and account management services
-
'';
-
};
+
enable = mkEnableOption ''
+
sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
+
task dispatching, wiki and account management services
+
'';
services = mkOption {
-
type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]);
-
default = [ "man" "meta" "paste" ];
-
example = [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ];
-
description = ''
-
Services to enable on the sourcehut network.
-
'';
-
};
-
-
originBase = mkOption {
-
type = types.str;
-
default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}";
-
defaultText = literalExpression ''
-
with config.networking; hostName + optionalString (domain != null) ".''${domain}"
-
'';
+
type = with types; listOf (enum
+
[ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
+
defaultText = "locally enabled services";
description = ''
-
Host name used by reverse-proxy and for default settings. Will host services at git."''${originBase}". For example: git.sr.ht
+
Services that may be displayed as links in the title bar of the Web interface.
'';
};
-
address = mkOption {
+
listenAddress = mkOption {
type = types.str;
-
default = "127.0.0.1";
-
description = ''
-
Address to bind to.
-
'';
+
default = "localhost";
+
description = "Address to bind to.";
};
python = mkOption {
···
'';
};
-
statePath = mkOption {
-
type = types.path;
-
default = "/var/lib/sourcehut";
-
description = ''
-
Root state path for the sourcehut network. If left as the default value
-
this directory will automatically be created before the sourcehut server
-
starts, otherwise the sysadmin is responsible for ensuring the
-
directory exists with appropriate ownership and permissions.
-
'';
+
minio = {
+
enable = mkEnableOption ''local minio integration'';
+
};
+
+
nginx = {
+
enable = mkEnableOption ''local nginx integration'';
+
virtualHost = mkOption {
+
type = types.attrs;
+
default = {};
+
description = "Virtual-host configuration merged with all Sourcehut's virtual-hosts.";
+
};
+
};
+
+
postfix = {
+
enable = mkEnableOption ''local postfix integration'';
+
};
+
+
postgresql = {
+
enable = mkEnableOption ''local postgresql integration'';
+
};
+
+
redis = {
+
enable = mkEnableOption ''local redis integration in a dedicated redis-server'';
};
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 <code>srht-keygen network</code> 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";
+
};
+
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 <code>srht-keygen service</code> 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 <code>gpg --edit-key [key-id]</code>,
+
then use the <code>passwd</code> 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 <code>srht-keygen webhook</code> 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" // {
+
allow-free = mkEnableOption "nonpaying users to submit builds";
+
redis = mkOption {
+
description = "The Redis connection used for the Celery worker.";
+
type = types.str;
+
default = "redis+socket:///run/redis-sourcehut-buildsrht/redis.sock?virtual_host=2";
+
};
+
shell = mkOption {
+
description = ''
+
Scripts used to launch on SSH connection.
+
<literal>/usr/bin/master-shell</literal> on master,
+
<literal>/usr/bin/runner-shell</literal> on runner.
+
If master and worker are on the same system
+
set to <literal>/usr/bin/runner-shell</literal>.
+
'';
+
type = types.enum ["/usr/bin/master-shell" "/usr/bin/runner-shell"];
+
default = "/usr/bin/master-shell";
+
};
+
};
+
options."builds.sr.ht::worker" = {
+
bind-address = mkOption {
+
description = ''
+
HTTP bind address for serving local build information/monitoring.
+
'';
+
type = types.str;
+
default = "localhost:8080";
+
};
+
buildlogs = mkOption {
+
description = "Path to write build logs.";
+
type = types.str;
+
default = "/var/log/sourcehut/buildsrht";
+
};
+
name = mkOption {
+
description = ''
+
Listening address and listening port
+
of the build runner (with HTTP port if not 80).
+
'';
+
type = types.str;
+
default = "localhost:5020";
+
};
+
timeout = mkOption {
+
description = ''
+
Max build duration.
+
See <link xlink:href="https://golang.org/pkg/time/#ParseDuration"/>.
+
'';
+
type = types.str;
+
default = "3m";
+
};
+
};
+
+
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-wrapper" ''
+
set -e
+
test -e "''${PWD%/*}"/config.ini ||
+
ln -s /run/sourcehut/gitsrht/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+socket:///run/redis-sourcehut-gitsrht/redis.sock?virtual_host=1";
+
};
+
};
+
options."git.sr.ht::api" = {
+
internal-ipnet = mkOption {
+
description = ''
+
Set of IP subnets which are permitted to utilize internal API
+
authentication. This should be limited to the subnets
+
from which your *.sr.ht services are running.
+
See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+
'';
+
type = with types; listOf str;
+
default = [ "127.0.0.0/8" "::1/128" ];
+
};
+
};
+
+
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";
+
defaultText = "\${cfg.python}/bin/hgsrht-hook-changegroup";
+
# Mercurial's changegroup hooks are run relative to their repository's directory,
+
# but hgsrht-hook-changegroup looks up ./config.ini
+
apply = p: pkgs.writeShellScript "hook-changegroup-wrapper" ''
+
set -e
+
test -e "''$PWD"/config.ini ||
+
ln -s /run/sourcehut/hgsrht/config.ini "''$PWD"/config.ini
+
exec -a "$0" '${p}' "$@"
+
'';
+
};
+
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";
+
defaultText = "\${pkgs.mercurial}/bin/hg-ssh";
+
};
+
webhooks = mkOption {
+
description = "The Redis connection used for the webhooks worker.";
+
type = types.str;
+
default = "redis+socket:///run/redis-sourcehut-hgsrht/redis.sock?virtual_host=1";
+
};
+
};
+
+
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+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=2";
+
};
+
webhooks = mkOption {
+
description = "The Redis connection used for the webhooks worker.";
+
type = types.str;
+
default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=1";
+
};
+
};
+
options."lists.sr.ht::worker" = {
+
reject-mimetypes = mkOption {
+
description = ''
+
Comma-delimited list of Content-Types to reject. Messages with Content-Types
+
included in this list are rejected. Multipart messages are always supported,
+
and each part is checked against this list.
+
+
Uses fnmatch for wildcard expansion.
+
'';
+
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://${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
+
defaultText = ''http://<xref linkend="opt-services.sourcehut.listenAddress"/>:''${toString (<xref linkend="opt-services.sourcehut.meta.port"/> + 100)}'';
+
};
+
webhooks = mkOption {
+
description = "The Redis connection used for the webhooks worker.";
+
type = types.str;
+
default = "redis+socket:///run/redis-sourcehut-metasrht/redis.sock?virtual_host=1";
+
};
+
welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
+
};
+
options."meta.sr.ht::api" = {
+
internal-ipnet = mkOption {
+
description = ''
+
Set of IP subnets which are permitted to utilize internal API
+
authentication. This should be limited to the subnets
+
from which your *.sr.ht services are running.
+
See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+
'';
+
type = with types; listOf str;
+
default = [ "127.0.0.0/8" "::1/128" ];
+
};
+
};
+
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."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."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 &lt;username&gt;.this.domain.
+
'';
+
type = with types; nullOr str;
+
default = null;
+
};
+
};
+
options."pages.sr.ht::api" = {
+
internal-ipnet = mkOption {
+
description = ''
+
Set of IP subnets which are permitted to utilize internal API
+
authentication. This should be limited to the subnets
+
from which your *.sr.ht services are running.
+
See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+
'';
+
type = with types; listOf str;
+
default = [ "127.0.0.0/8" "::1/128" ];
+
};
+
};
+
+
options."paste.sr.ht" = commonServiceSettings "paste" // {
+
};
+
+
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+socket:///run/redis-sourcehut-todosrht/redis.sock?virtual_host=1";
+
};
+
};
+
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.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 = (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
+
pkgs = (import pkgs_unstable {});
+
});
+
in
+
{
+
nixos.unstable.x86_64 = image_from_nixpkgs;
+
}
+
)'';
+
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 = literalExpression "pkgs.gitFull";
+
description = ''
+
Git package for git.sr.ht. This can help silence collisions.
+
'';
+
};
+
fcgiwrap.preforkProcess = mkOption {
+
description = "Number of fcgiwrap processes to prefork.";
+
type = types.int;
+
default = 4;
+
};
+
};
+
+
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).
+
'';
+
};
+
};
+
+
lists = {
+
process = {
+
extraArgs = mkOption {
+
type = with types; listOf str;
+
default = [ "--loglevel DEBUG" "--pool eventlet" "--without-heartbeat" ];
+
description = "Extra arguments passed to the Celery responsible for processing mails.";
+
};
+
celeryConfig = mkOption {
+
type = types.lines;
+
default = "";
+
description = "Content of the <literal>celeryconfig.py</literal> used by the Celery of <literal>listssrht-process</literal>.";
+
};
+
};
+
};
};
-
config = mkIf cfg.enable {
-
assertions =
-
[
+
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 {
+
assertions = [
+
{ assertion = postgresql.enable;
+
message = "postgresql must be enabled and configured";
+
}
+
];
+
})
+
(mkIf cfg.postfix.enable {
+
assertions = [
+
{ assertion = postfix.enable;
+
message = "postfix must be enabled and configured";
+
}
+
];
+
# Needed for sharing the LMTP sockets with JoinsNamespaceOf=
+
systemd.services.postfix.serviceConfig.PrivateTmp = true;
+
})
+
(mkIf cfg.redis.enable {
+
services.redis.vmOverCommit = mkDefault true;
+
})
+
(mkIf cfg.nginx.enable {
+
assertions = [
+
{ assertion = nginx.enable;
+
message = "nginx must be enabled and configured";
+
}
+
];
+
# For proxyPass= in virtual-hosts for Sourcehut services.
+
services.nginx.recommendedProxySettings = mkDefault true;
+
})
+
(mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
+
services.openssh = {
+
# Note that sshd will continue to honor AuthorizedKeysFile.
+
# Note that you may want automatically rotate
+
# or link to /dev/null the following log files:
+
# - /var/log/gitsrht-dispatch
+
# - /var/log/{build,git,hg}srht-keys
+
# - /var/log/{git,hg}srht-shell
+
# - /var/log/gitsrht-update-hook
+
authorizedKeysCommand = ''/etc/ssh/sourcehut/subdir/srht-dispatch "%u" "%h" "%t" "%k"'';
+
# srht-dispatch will setuid/setgid according to [git.sr.ht::dispatch]
+
authorizedKeysCommandUser = "root";
+
extraConfig = ''
+
PermitUserEnvironment SRHT_*
+
'';
+
};
+
environment.etc."ssh/sourcehut/config.ini".source =
+
settingsFormat.generate "sourcehut-dispatch-config.ini"
+
(filterAttrs (k: v: k == "git.sr.ht::dispatch")
+
cfg.settings);
+
environment.etc."ssh/sourcehut/subdir/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 /etc/ssh/sourcehut/subdir
+
${cfg.python}/bin/gitsrht-dispatch "$@"
+
'';
+
};
+
systemd.services.sshd = {
+
#path = optional cfg.git.enable [ cfg.git.package ];
+
serviceConfig = {
+
BindReadOnlyPaths =
+
# Note that those /usr/bin/* paths are hardcoded in multiple places in *.sr.ht,
+
# for instance to get the user from the [git.sr.ht::dispatch] settings.
+
# *srht-keys needs to:
+
# - access a redis-server in [sr.ht] redis-host,
+
# - access the PostgreSQL server in [*.sr.ht] connection-string,
+
# - query metasrht-api (through the HTTP API).
+
# Using this has the side effect of creating empty files in /usr/bin/
+
optionals cfg.builds.enable [
+
"${pkgs.writeShellScript "buildsrht-keys-wrapper" ''
+
set -ex
+
cd /run/sourcehut/buildsrht/subdir
+
exec -a "$0" ${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys "$@"
+
''}:/usr/bin/buildsrht-keys"
+
"${pkgs.sourcehut.buildsrht}/bin/master-shell:/usr/bin/master-shell"
+
"${pkgs.sourcehut.buildsrht}/bin/runner-shell:/usr/bin/runner-shell"
+
] ++
+
optionals cfg.git.enable [
+
# /path/to/gitsrht-keys calls /path/to/gitsrht-shell,
+
# or [git.sr.ht] shell= if set.
+
"${pkgs.writeShellScript "gitsrht-keys-wrapper" ''
+
set -ex
+
cd /run/sourcehut/gitsrht/subdir
+
exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys "$@"
+
''}:/usr/bin/gitsrht-keys"
+
"${pkgs.writeShellScript "gitsrht-shell-wrapper" ''
+
set -e
+
cd /run/sourcehut/gitsrht/subdir
+
exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell "$@"
+
''}:/usr/bin/gitsrht-shell"
+
] ++
+
optionals cfg.hg.enable [
+
# /path/to/hgsrht-keys calls /path/to/hgsrht-shell,
+
# or [hg.sr.ht] shell= if set.
+
"${pkgs.writeShellScript "hgsrht-keys-wrapper" ''
+
set -ex
+
cd /run/sourcehut/hgsrht/subdir
+
exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys "$@"
+
''}:/usr/bin/hgsrht-keys"
+
":/usr/bin/hgsrht-shell"
+
"${pkgs.writeShellScript "hgsrht-shell-wrapper" ''
+
set -e
+
cd /run/sourcehut/hgsrht/subdir
+
exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell "$@"
+
''}:/usr/bin/hgsrht-shell"
+
];
+
};
+
};
+
})
+
]);
+
+
imports = [
+
+
(import ./service.nix "builds" {
+
inherit configIniOfService;
+
srvsrht = "buildsrht";
+
port = 5002;
+
# TODO: a celery worker on the master and worker are apparently needed
+
extraServices.buildsrht-worker = let
+
qemuPackage = pkgs.qemu_kvm;
+
serviceName = "buildsrht-worker";
+
statePath = "/var/lib/sourcehut/${serviceName}";
+
in mkIf cfg.builds.enableWorker {
+
path = [ pkgs.openssh pkgs.docker ];
+
preStart = ''
+
set -x
+
if test -z "$(docker images -q qemu:latest 2>/dev/null)" \
+
|| test "$(cat ${statePath}/docker-image-qemu)" != "${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
+
echo '${qemuPackage.version}' >${statePath}/docker-image-qemu
+
fi
+
'';
+
serviceConfig = {
+
ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
+
RuntimeDirectory = [ "sourcehut/${serviceName}/subdir" ];
+
# builds.sr.ht-worker looks up ../config.ini
+
LogsDirectory = [ "sourcehut/${serviceName}" ];
+
StateDirectory = [ "sourcehut/${serviceName}" ];
+
WorkingDirectory = "-"+"/run/sourcehut/${serviceName}/subdir";
+
};
+
};
+
extraConfig = let
+
image_dirs = flatten (
+
mapAttrsToList (distro: revs:
+
mapAttrsToList (rev: archs:
+
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
+
) cfg.builds.images
+
);
+
image_dir_pre = pkgs.symlinkJoin {
+
name = "builds.sr.ht-worker-images-pre";
+
paths = image_dirs;
+
# FIXME: not working, apparently because ubuntu/latest is a broken link
+
# ++ [ "${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 mkMerge [
+
{
+
users.users.${cfg.builds.user}.shell = pkgs.bash;
+
+
virtualisation.docker.enable = true;
+
+
services.sourcehut.settings = mkMerge [
+
{ # Note that git.sr.ht::dispatch is not a typo,
+
# gitsrht-dispatch always use this section
+
"git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
+
mkDefault "${cfg.builds.user}:${cfg.builds.group}";
+
}
+
(mkIf cfg.builds.enableWorker {
+
"builds.sr.ht::worker".shell = "/usr/bin/runner-shell";
+
"builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
+
"builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
+
})
+
];
+
}
+
(mkIf cfg.builds.enableWorker {
+
users.groups = {
+
docker.members = [ cfg.builds.user ];
+
};
+
})
+
(mkIf (cfg.builds.enableWorker && cfg.nginx.enable) {
+
# Allow nginx access to buildlogs
+
users.users.${nginx.user}.extraGroups = [ cfg.builds.group ];
+
systemd.services.nginx = {
+
serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."builds.sr.ht::worker".buildlogs}:/var/log/nginx/buildsrht/logs" ];
+
};
+
services.nginx.virtualHosts."logs.${domain}" = mkMerge [ {
+
/* FIXME: is a listen needed?
+
listen = with builtins;
+
# FIXME: not compatible with IPv6
+
let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
+
[{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
+
*/
+
locations."/logs/".alias = "/var/log/nginx/buildsrht/logs/";
+
} cfg.nginx.virtualHost ];
+
})
+
];
+
})
+
+
(import ./service.nix "dispatch" {
+
inherit configIniOfService;
+
port = 5005;
+
})
+
+
(import ./service.nix "git" (let
+
baseService = {
+
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;
+
mainService = mkMerge [ baseService {
+
serviceConfig.StateDirectory = [ "sourcehut/gitsrht" "sourcehut/gitsrht/repos" ];
+
} ];
+
port = 5001;
+
webhooks = true;
+
extraTimers.gitsrht-periodic = {
+
service = baseService;
+
timerConfig.OnCalendar = ["20min"];
+
};
+
extraConfig = mkMerge [
+
{
+
# 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;
+
services.sourcehut.settings = {
+
"git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
+
mkDefault "${cfg.git.user}:${cfg.git.group}";
+
};
+
systemd.services.sshd = baseService;
+
}
+
(mkIf cfg.nginx.enable {
+
services.nginx.virtualHosts."git.${domain}" = {
+
locations."/authorize" = {
+
proxyPass = "http://${cfg.listenAddress}:${toString cfg.git.port}";
+
extraConfig = ''
+
proxy_pass_request_body off;
+
proxy_set_header Content-Length "";
+
proxy_set_header X-Original-URI $request_uri;
+
'';
+
};
+
locations."~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$" = {
+
root = "/var/lib/sourcehut/gitsrht/repos";
+
fastcgiParams = {
+
GIT_HTTP_EXPORT_ALL = "";
+
GIT_PROJECT_ROOT = "$document_root";
+
PATH_INFO = "$uri";
+
SCRIPT_FILENAME = "${cfg.git.package}/bin/git-http-backend";
+
};
+
extraConfig = ''
+
auth_request /authorize;
+
fastcgi_read_timeout 500s;
+
fastcgi_pass unix:/run/gitsrht-fcgiwrap.sock;
+
gzip off;
+
'';
+
};
+
};
+
systemd.sockets.gitsrht-fcgiwrap = {
+
before = [ "nginx.service" ];
+
wantedBy = [ "sockets.target" "gitsrht.service" ];
+
# This path remains accessible to nginx.service, which has no RootDirectory=
+
socketConfig.ListenStream = "/run/gitsrht-fcgiwrap.sock";
+
socketConfig.SocketUser = nginx.user;
+
socketConfig.SocketMode = "600";
+
};
+
})
+
];
+
extraServices.gitsrht-fcgiwrap = mkIf cfg.nginx.enable {
+
serviceConfig = {
+
# Socket is passed by gitsrht-fcgiwrap.socket
+
ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${toString cfg.git.fcgiwrap.preforkProcess}";
+
# No need for config.ini
+
ExecStartPre = mkForce [];
+
User = null;
+
DynamicUser = true;
+
BindReadOnlyPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
+
IPAddressDeny = "any";
+
InaccessiblePaths = [ "-+/run/postgresql" "-+/run/redis-sourcehut" ];
+
PrivateNetwork = true;
+
RestrictAddressFamilies = mkForce [ "none" ];
+
SystemCallFilter = mkForce [
+
"@system-service"
+
"~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@setuid"
+
# @timer is needed for alarm()
+
];
+
};
+
};
+
}))
+
+
(import ./service.nix "hg" (let
+
baseService = {
+
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;
+
mainService = mkMerge [ baseService {
+
serviceConfig.StateDirectory = [ "sourcehut/hgsrht" "sourcehut/hgsrht/repos" ];
+
} ];
+
port = 5010;
+
webhooks = true;
+
extraTimers.hgsrht-periodic = {
+
service = baseService;
+
timerConfig.OnCalendar = ["20min"];
+
};
+
extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
+
service = baseService;
+
timerConfig.OnCalendar = ["daily"];
+
timerConfig.AccuracySec = "1h";
+
};
+
extraConfig = mkMerge [
{
-
assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44;
-
message = "The webhook's private key must be defined and of a 44 byte length.";
+
users.users.${cfg.hg.user}.shell = pkgs.bash;
+
services.sourcehut.settings = {
+
# Note that git.sr.ht::dispatch is not a typo,
+
# gitsrht-dispatch always uses this section.
+
"git.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
+
mkDefault "${cfg.hg.user}:${cfg.hg.group}";
+
};
+
systemd.services.sshd = baseService;
}
+
(mkIf cfg.nginx.enable {
+
# Allow nginx access to repositories
+
users.users.${nginx.user}.extraGroups = [ cfg.hg.group ];
+
services.nginx.virtualHosts."hg.${domain}" = {
+
locations."/authorize" = {
+
proxyPass = "http://${cfg.listenAddress}:${toString cfg.hg.port}";
+
extraConfig = ''
+
proxy_pass_request_body off;
+
proxy_set_header Content-Length "";
+
proxy_set_header X-Original-URI $request_uri;
+
'';
+
};
+
# Let clients reach pull bundles. We don't really need to lock this down even for
+
# private repos because the bundles are named after the revision hashes...
+
# so someone would need to know or guess a SHA value to download anything.
+
# TODO: proxyPass to an hg serve service?
+
locations."~ ^/[~^][a-z0-9_]+/[a-zA-Z0-9_.-]+/\\.hg/bundles/.*$" = {
+
root = "/var/lib/nginx/hgsrht/repos";
+
extraConfig = ''
+
auth_request /authorize;
+
gzip off;
+
'';
+
};
+
};
+
systemd.services.nginx = {
+
serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/nginx/hgsrht/repos" ];
+
};
+
})
+
];
+
}))
+
(import ./service.nix "hub" {
+
inherit configIniOfService;
+
port = 5014;
+
extraConfig = {
+
services.nginx = mkIf cfg.nginx.enable {
+
virtualHosts."hub.${domain}" = mkMerge [ {
+
serverAliases = [ domain ];
+
} cfg.nginx.virtualHost ];
+
};
+
};
+
})
+
+
(import ./service.nix "lists" (let
+
srvsrht = "listssrht";
+
in {
+
inherit configIniOfService;
+
port = 5006;
+
webhooks = true;
+
# Receive the mail from Postfix and enqueue them into Redis and PostgreSQL
+
extraServices.listssrht-lmtp = {
+
wants = [ "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;
+
};
+
# Dequeue the mails from Redis and dispatch them
+
extraServices.listssrht-process = {
+
serviceConfig = {
+
preStart = ''
+
cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" cfg.lists.process.celeryConfig} \
+
/run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
+
'';
+
ExecStart = "${cfg.python}/bin/celery --app listssrht.process worker --hostname listssrht-process@%%h " + concatStringsSep " " cfg.lists.process.extraArgs;
+
# Avoid crashing: os.getloadavg()
+
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 = {
+
destination = [ "lists.${domain}" ];
+
# FIXME: an accurate recipient list should be queried
+
# from the lists.sr.ht PostgreSQL database to avoid backscattering.
+
# But usernames are unfortunately not in that database but in meta.sr.ht.
+
# Note that two syntaxes are allowed:
+
# - ~username/list-name@lists.${domain}
+
# - u.username.list-name@lists.${domain}
+
localRecipients = [ "@lists.${domain}" ];
+
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 = true;
+
extraServices.metasrht-api = {
+
serviceConfig.Restart = "always";
+
serviceConfig.RestartSec = "2s";
+
preStart = "set -x\n" + concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
+
let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
+
srv = head srvMatch;
+
in
+
# Configure client(s) as "preauthorized"
+
optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
+
# Configure ${srv}'s OAuth client as "preauthorized"
+
${postgresql.package}/bin/psql '${cfg.settings."meta.sr.ht".connection-string}' \
+
-c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
+
''
+
) cfg.settings));
+
serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
+
};
+
extraTimers.metasrht-daily.timerConfig = {
+
OnCalendar = ["daily"];
+
AccuracySec = "1h";
+
};
+
extraConfig = mkMerge [
{
-
assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null;
-
message = "meta.sr.ht's origin must be defined.";
+
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 = optional cfg.meta.enable
+
(pkgs.writeShellScriptBin "metasrht-manageuser" ''
+
set -eux
+
if test "$(${pkgs.coreutils}/bin/id -n -u)" != '${cfg.meta.user}'
+
then exec sudo -u '${cfg.meta.user}' "$0" "$@"
+
else
+
# In order to load config.ini
+
if cd /run/sourcehut/metasrht
+
then exec ${cfg.python}/bin/metasrht-manageuser "$@"
+
else cat <<EOF
+
Please run: sudo systemctl start metasrht
+
EOF
+
exit 1
+
fi
+
fi
+
'');
}
+
(mkIf cfg.nginx.enable {
+
services.nginx.virtualHosts."meta.${domain}" = {
+
locations."/query" = {
+
proxyPass = cfg.settings."meta.sr.ht".api-origin;
+
extraConfig = ''
+
if ($request_method = 'OPTIONS') {
+
add_header 'Access-Control-Allow-Origin' '*';
+
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+
add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+
add_header 'Access-Control-Max-Age' 1728000;
+
add_header 'Content-Type' 'text/plain; charset=utf-8';
+
add_header 'Content-Length' 0;
+
return 204;
+
}
+
+
add_header 'Access-Control-Allow-Origin' '*';
+
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+
add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
+
'';
+
};
+
};
+
})
];
+
})
-
virtualisation.docker.enable = true;
-
environment.etc."sr.ht/config.ini".source =
-
settingsFormat.generate "sourcehut-config.ini" (mapAttrsRecursive
-
(
-
path: v: if v == null then "" else v
-
)
-
cfg.settings);
+
(import ./service.nix "pages" {
+
inherit configIniOfService;
+
port = 5112;
+
mainService = let
+
srvsrht = "pagessrht";
+
version = pkgs.sourcehut.${srvsrht}.version;
+
stateDir = "/var/lib/sourcehut/${srvsrht}";
+
iniKey = "pages.sr.ht";
+
in {
+
preStart = mkBefore ''
+
set -x
+
# Use the /run/sourcehut/${srvsrht}/config.ini
+
# installed by a previous ExecStartPre= in baseService
+
cd /run/sourcehut/${srvsrht}
-
environment.systemPackages = [ pkgs.sourcehut.coresrht ];
+
if test ! -e ${stateDir}/db; then
+
${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
+
echo ${version} >${stateDir}/db
+
fi
-
# PostgreSQL server
-
services.postgresql.enable = mkOverride 999 true;
-
# Mail server
-
services.postfix.enable = mkOverride 999 true;
-
# Cron daemon
-
services.cron.enable = mkOverride 999 true;
-
# Redis server
-
services.redis.enable = mkOverride 999 true;
-
services.redis.bind = mkOverride 999 "127.0.0.1";
+
${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
+
''}
-
services.sourcehut.settings = {
-
# The name of your network of sr.ht-based sites
-
"sr.ht".site-name = mkDefault "sourcehut";
-
# The top-level info page for your site
-
"sr.ht".site-info = mkDefault "https://sourcehut.org";
-
# {{ site-name }}, {{ site-blurb }}
-
"sr.ht".site-blurb = mkDefault "the hacker's forge";
-
# If this != production, we add a banner to each page
-
"sr.ht".environment = mkDefault "development";
-
# Contact information for the site owners
-
"sr.ht".owner-name = mkDefault "Drew DeVault";
-
"sr.ht".owner-email = mkDefault "sir@cmpwn.com";
-
# The source code for your fork of sr.ht
-
"sr.ht".source-url = mkDefault "https://git.sr.ht/~sircmpwn/srht";
-
# A secret key to encrypt session cookies with
-
"sr.ht".secret-key = mkDefault null;
-
"sr.ht".global-domain = mkDefault null;
+
# Disable webhook
+
touch ${stateDir}/webhook
+
'';
+
serviceConfig = {
+
ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
+
};
+
};
+
})
-
# Outgoing SMTP settings
-
mail.smtp-host = mkDefault null;
-
mail.smtp-port = mkDefault null;
-
mail.smtp-user = mkDefault null;
-
mail.smtp-password = mkDefault null;
-
mail.smtp-from = mkDefault null;
-
# Application exceptions are emailed to this address
-
mail.error-to = mkDefault null;
-
mail.error-from = mkDefault null;
-
# 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.
-
mail.pgp-privkey = mkDefault null;
-
mail.pgp-pubkey = mkDefault null;
-
mail.pgp-key-id = mkDefault null;
+
(import ./service.nix "paste" {
+
inherit configIniOfService;
+
port = 5011;
+
})
-
# base64-encoded Ed25519 key for signing webhook payloads. This should be
-
# consistent for all *.sr.ht sites, as we'll use this key to verify signatures
-
# from other sites in your network.
-
#
-
# Use the srht-webhook-keygen command to generate a key.
-
webhooks.private-key = mkDefault null;
-
};
-
};
+
(import ./service.nix "todo" {
+
inherit configIniOfService;
+
port = 5003;
+
webhooks = true;
+
extraServices.todosrht-lmtp = {
+
wants = [ "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 = {
+
destination = [ "todo.${domain}" ];
+
# FIXME: an accurate recipient list should be queried
+
# from the todo.sr.ht PostgreSQL database to avoid backscattering.
+
# But usernames are unfortunately not in that database but in meta.sr.ht.
+
# Note that two syntaxes are allowed:
+
# - ~username/tracker-name@todo.${domain}
+
# - u.username.tracker-name@todo.${domain}
+
localRecipients = [ "@todo.${domain}" ];
+
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; [ tomberek ];
+
meta.maintainers = with maintainers; [ julm tomberek ];
}
+362 -53
nixos/modules/services/misc/sourcehut/service.nix
···
-
{ config, pkgs, lib }:
-
serviceCfg: serviceDrv: iniKey: attrs:
+
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;
-
cfgIni = cfg.settings."${iniKey}";
-
pgSuperUser = config.services.postgresql.superUser;
-
-
setupDB = pkgs.writeScript "${serviceDrv.pname}-gen-db" ''
-
#! ${cfg.python}/bin/python
-
from ${serviceDrv.pname}.app import db
-
db.create()
-
'';
+
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
-
with serviceCfg; with lib; recursiveUpdate
{
-
environment.HOME = statePath;
-
path = [ config.services.postgresql.package ] ++ (attrs.path or [ ]);
-
restartTriggers = [ config.environment.etc."sr.ht/config.ini".source ];
-
serviceConfig = {
-
Type = "simple";
-
User = user;
-
Group = user;
-
Restart = "always";
-
WorkingDirectory = statePath;
-
} // (if (cfg.statePath == "/var/lib/sourcehut/${serviceDrv.pname}") then {
-
StateDirectory = [ "sourcehut/${serviceDrv.pname}" ];
-
} else {})
-
;
+
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 ];
+
};
+
};
-
preStart = ''
-
if ! test -e ${statePath}/db; then
-
# Setup the initial database
-
${setupDB}
+
services.nginx = mkIf cfg.nginx.enable {
+
virtualHosts."${srv}.${cfg.settings."sr.ht".global-domain}" = mkMerge [ {
+
forceSSL = 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}
-
# Set the initial state of the database for future database upgrades
-
if test -e ${cfg.python}/bin/${serviceDrv.pname}-migrate; then
-
# Run alembic stamp head once to tell alembic the schema is up-to-date
-
${cfg.python}/bin/${serviceDrv.pname}-migrate stamp head
-
fi
+
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
-
printf "%s" "${serviceDrv.version}" > ${statePath}/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 ${statePath}/webhook; then
-
# Update ${iniKey}'s users' profile copy to the latest
-
${cfg.python}/bin/srht-update-profiles ${iniKey}
+
# 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 ]);
+
}
-
touch ${statePath}/webhook
-
fi
+
(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";
+
};
+
};
+
})
-
${optionalString (builtins.hasAttr "migrate-on-upgrade" cfgIni && cfgIni.migrate-on-upgrade == "yes") ''
-
if [ "$(cat ${statePath}/db)" != "${serviceDrv.version}" ]; then
-
# Manage schema migrations using alembic
-
${cfg.python}/bin/${serviceDrv.pname}-migrate -a upgrade head
+
(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)
-
# Mark down current package version
-
printf "%s" "${serviceDrv.version}" > ${statePath}/db
-
fi
-
''}
+
(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)
+
];
-
${attrs.preStart or ""}
-
'';
+
systemd.timers = mapAttrs (timerName: timer:
+
{
+
description = "sourcehut timer for ${timerName}";
+
wantedBy = [ "timers.target" ];
+
inherit (timer) timerConfig;
+
}) extraTimers;
+
} ]);
}
-
(builtins.removeAttrs attrs [ "path" "preStart" ])
+14 -10
nixos/modules/services/misc/sourcehut/sourcehut.xml
···
<title>Basic usage</title>
<para>
Sourcehut is a Python and Go based set of applications.
-
<literal><link linkend="opt-services.sourcehut.enable">services.sourcehut</link></literal>
-
by default will use
+
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.nginx.enable">services.redis</link></literal>,
-
<literal><link linkend="opt-services.nginx.enable">services.cron</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>.
+
<literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal> services.
</para>
<para>
···
services.sourcehut = {
<link linkend="opt-services.sourcehut.enable">enable</link> = true;
-
<link linkend="opt-services.sourcehut.originBase">originBase</link> = fqdn;
-
<link linkend="opt-services.sourcehut.services">services</link> = [ "meta" "man" "git" ];
+
<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 = "SECRET";
-
service-key = "SECRET";
+
network-key = "/run/keys/path/to/network-key";
+
service-key = "/run/keys/path/to/service-key";
};
-
webhooks.private-key= "SECRET";
+
webhooks.private-key= "/run/keys/path/to/webhook-key";
};
};