{ config, pkgs, lib, ... }:
-
with lib;
let
+ inherit (config.services) nginx postfix postgresql redis;
+ inherit (config.users) users groups;
cfg = config.services.sourcehut;
- rcfg = config.services.redis;
- cfgIni = cfg.settings;
+ domain = cfg.settings."sr.ht".global-domain;
settingsFormat = pkgs.formats.ini {
listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
mkKeyValue = k: v:
else generators.mkValueStringDefault {} v;
} "=" k v;
};
- commonServiceSettings = service: {
+ 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 = "/usr/bin/gitsrht-update-hook";
+ "git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
+ "hg.sr.ht".changegroup-script = "/usr/bin/hgsrht-hook-changegroup";
+ "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 ${service}.sr.ht is being served at (protocol://domain)";
+ description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
type = types.str;
- default = "https://${service}.${cfg.originBase}";
+ default = "https://${srv}.${domain}";
+ defaultText = "https://${srv}.example.com";
};
debug-host = mkOption {
description = "Address to bind the debug server to.";
- type = types.str;
- default = "0.0.0.0";
+ type = with types; nullOr str;
+ default = null;
};
debug-port = mkOption {
description = "Port to bind the debug server to.";
- type = types.port;
- default = cfg.${service}.port;
+ type = with types; nullOr str;
+ default = null;
};
connection-string = mkOption {
description = "SQLAlchemy connection string for the database.";
type = types.str;
- default = "postgresql:///${cfg.${service}.database}?user=${cfg.${service}.user}&host=/var/run/postgresql";
+ 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 = "${service}.sr.ht's OAuth client id for meta.sr.ht.";
+ description = "${srv}.sr.ht's OAuth client id for meta.sr.ht.";
type = types.str;
};
oauth-client-secret = mkOption {
- description = "${service}.sr.ht's OAuth client secret for meta.sr.ht.";
- type = types.str;
+ description = "${srv}.sr.ht's OAuth client secret for meta.sr.ht.";
+ type = types.path;
+ apply = s: "<" + toString s;
};
};
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
]);
};
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.
- '')
- (mkRemovedOptionModule [ "services" "sourcehut" "services" ] ''
- Please use the `config.services.sourcehut.''${service}.enable' options instead.
- '')
- ];
-
options.services.sourcehut = {
enable = mkEnableOption ''
sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
task dispatching, wiki and account management services
'';
- originBase = mkOption {
- type = types.str;
- default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}";
+ services = mkOption {
+ 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 {
'';
};
+ 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";
};
- global-domain = mkOptionNullOrStr "Global domain name.";
+ 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;
type = types.str;
default = "John Doe";
};
- secret-key = mkOptionNullOrStr "Secret key to encrypt session cookies with.";
site-blurb = mkOption {
description = "Blurb for your site.";
type = types.str;
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;
error-to = mkOptionNullOrStr "Address receiving application exceptions";
error-from = mkOptionNullOrStr "Address sending application exceptions";
pgp-privkey = mkOptionNullOrStr ''
- OpenPGP private key.
+ 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.
+ 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 = mkOptionNullOrStr ''
- 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-webhook-keygen</code> command to generate a key.
- '';
+ 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" = {
default = "./repo-cache";
};
"gitlab.com" = mkOption {
- type = types.str;
+ type = with types; nullOr str;
description = "GitLab id and secret.";
- default = "";
+ 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.";
+ description = "The Redis connection used for the Celery worker.";
type = types.str;
- default = "redis://${rcfg.bind}:${toString rcfg.port}/3";
+ default = "redis+socket:///run/redis-sourcehut-buildsrht/redis.sock?virtual_host=2";
};
shell = mkOption {
- description = "The shell used for ssh.";
+ 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 = "runner-shell";
+ 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 = "http://git.${cfg.originBase}";
+ default = "https://git.localhost.localdomain";
};
post-update-script = mkOption {
- description = "A post-update script which is installed in every git repo.";
- type = types.str;
+ 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";
};
repos = mkOption {
- description = "Path to git repositories on disk.";
+ 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/git";
+ default = "/var/lib/sourcehut/gitsrht/repos";
};
webhooks = mkOption {
- description = "The redis connection used for the webhooks worker.";
+ description = "The Redis connection used for the webhooks worker.";
type = types.str;
- default = "redis://${rcfg.bind}:${toString rcfg.port}/1";
+ 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 post-update script which is installed in every mercurial repo..";
+ 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";
};
repos = mkOption {
- description = "Path to mercurial repositories on disk.";
+ 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/hg";
+ default = "/var/lib/sourcehut/hgsrht/repos";
};
srhtext = mkOptionNullOrStr ''
Path to the srht mercurial extension
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.";
+ description = "The Redis connection used for the webhooks worker.";
type = types.str;
- default = "redis://${rcfg.bind}:${toString rcfg.port}/1";
+ 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.";
- network-key = mkOptionNullOrStr "Network key.";
notify-from = mkOption {
description = "Outgoing email for notifications generated by users.";
type = types.str;
- default = "lists-notify@${cfg.originBase}";
+ default = "lists-notify@localhost.localdomain";
};
posting-domain = mkOption {
description = "Posting domain.";
type = types.str;
- default = "lists.${cfg.originBase}";
+ default = "lists.localhost.localdomain";
};
redis = mkOption {
- description = "The redis connection used for the celery worker.";
+ description = "The Redis connection used for the Celery worker.";
type = types.str;
- default = "redis://${rcfg.bind}:${toString rcfg.port}/4";
+ default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=2";
};
webhooks = mkOption {
- description = "The redis connection used for the webhooks worker.";
+ description = "The Redis connection used for the webhooks worker.";
type = types.str;
- default = "redis://${rcfg.bind}:${toString rcfg.port}/2";
+ 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.";
- default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
type = types.str;
+ default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
};
sock = mkOption {
description = ''
default = "postfix";
};
};
+
options."man.sr.ht" = commonServiceSettings "man" // {
};
- options."meta.sr.ht" = commonServiceSettings "meta" // {
- oauth-client-id = mkOptionNullOrStr "OAuth client id.";
- oauth-client-secret = mkOptionNullOrStr "OAuth client secret.";
+
+ options."meta.sr.ht" =
+ removeAttrs (commonServiceSettings "meta")
+ ["oauth-client-id" "oauth-client-secret"] // {
api-origin = mkOption {
description = "Origin URL for API, 100 more than web.";
type = types.str;
- default = "http://localhost:5100";
+ 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.";
+ description = "The Redis connection used for the webhooks worker.";
type = types.str;
- default = "redis://${rcfg.bind}:${toString rcfg.port}/6";
+ 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.${cfg.originBase}";
+ default = "https://meta.localhost.localdomain";
};
user-invites = mkOption {
description = ''
default = 5;
};
};
- options."meta.sr.ht::aliases" = mkOption {
- description = "Aliases for the client IDs of commonly used OAuth clients.";
- type = with types; attrsOf int;
- default = {};
- example = { "git.sr.ht" = 12345; };
+
+ options."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 <username>.this.domain.
+ '';
+ type = with types; nullOr str;
+ default = null;
+ };
};
- 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 "Secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
+ 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" // {
- webhooks = mkOption {
- type = types.str;
- default = "redis://${rcfg.bind}:${toString rcfg.port}/5";
- };
};
+
options."todo.sr.ht" = commonServiceSettings "todo" // {
- network-key = mkOptionNullOrStr "Network key.";
notify-from = mkOption {
description = "Outgoing email for notifications generated by users.";
type = types.str;
- default = "todo-notify@${cfg.originBase}";
+ default = "todo-notify@localhost.localdomain";
};
webhooks = mkOption {
- description = "The redis connection used for the webhooks worker.";
+ description = "The Redis connection used for the webhooks worker.";
type = types.str;
- default = "redis://${rcfg.bind}:${toString rcfg.port}/7";
+ 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.${cfg.originBase}";
+ default = "todo.localhost.localdomain";
};
sock = mkOption {
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 -e
+ cd /run/sourcehut/buildsrht/subdir
+ set -x
+ 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 -e
+ cd /run/sourcehut/gitsrht/subdir
+ set -x
+ 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
+ set -x
+ exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell "$@"
+ ''}:/usr/bin/gitsrht-shell"
+ "${pkgs.writeShellScript "gitsrht-update-hook" ''
+ set -e
+ test -e "''${PWD%/*}"/config.ini ||
+ # Git hooks are run relative to their repository's directory,
+ # but gitsrht-update-hook looks up ../config.ini
+ ln -s /run/sourcehut/gitsrht/config.ini "''${PWD%/*}"/config.ini
+ # hooks/post-update calls /usr/bin/gitsrht-update-hook as hooks/stage-3
+ # but this wrapper being a bash script, it overrides $0 with /usr/bin/gitsrht-update-hook
+ # hence this hack to put hooks/stage-3 back into gitsrht-update-hook's $0
+ if test "''${STAGE3:+set}"
+ then
+ set -x
+ exec -a hooks/stage-3 ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
+ else
+ export STAGE3=set
+ set -x
+ exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
+ fi
+ ''}:/usr/bin/gitsrht-update-hook"
+ ] ++
+ 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 -e
+ cd /run/sourcehut/hgsrht/subdir
+ set -x
+ exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys "$@"
+ ''}:/usr/bin/hgsrht-keys"
+ "${pkgs.writeShellScript "hgsrht-shell-wrapper" ''
+ set -e
+ cd /run/sourcehut/hgsrht/subdir
+ set -x
+ exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell "$@"
+ ''}:/usr/bin/hgsrht-shell"
+ # Mercurial's changegroup hooks are run relative to their repository's directory,
+ # but hgsrht-hook-changegroup looks up ./config.ini
+ "${pkgs.writeShellScript "hgsrht-hook-changegroup" ''
+ set -e
+ test -e "''$PWD"/config.ini ||
+ ln -s /run/sourcehut/hgsrht/config.ini "''$PWD"/config.ini
+ set -x
+ exec -a "$0" ${cfg.python}/bin/hgsrht-hook-changegroup "$@"
+ ''}:/usr/bin/hgsrht-hook-changegroup"
+ ];
+ };
+ };
+ })
+ ]);
+
+ 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" ];
+ };
+ in {
+ inherit configIniOfService;
+ mainService = mkMerge [ baseService {
+ serviceConfig.StateDirectory = [ "sourcehut/gitsrht" "sourcehut/gitsrht/repos" ];
+ preStart = mkIf (!versionAtLeast config.system.stateVersion "21.11") (mkBefore ''
+ # Fix Git hooks of repositories pre-dating https://github.com/NixOS/nixpkgs/pull/133984
+ (
+ set +f
+ shopt -s nullglob
+ for h in /var/lib/sourcehut/gitsrht/repos/~*/*/hooks/{pre-receive,update,post-update}
+ do ln -fnsv /usr/bin/gitsrht-update-hook "$h"; done
+ )
+ '');
+ } ];
+ port = 5001;
+ webhooks = true;
+ extraTimers.gitsrht-periodic = {
+ service = baseService;
+ timerConfig.OnCalendar = ["*:0/20"];
+ };
+ 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.";
+ # 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" ];
+ };
+ in {
+ inherit configIniOfService;
+ mainService = mkMerge [ baseService {
+ serviceConfig.StateDirectory = [ "sourcehut/hgsrht" "sourcehut/hgsrht/repos" ];
+ } ];
+ port = 5010;
+ webhooks = true;
+ extraTimers.hgsrht-periodic = {
+ service = baseService;
+ timerConfig.OnCalendar = ["*:0/20"];
+ };
+ extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
+ service = baseService;
+ timerConfig.OnCalendar = ["daily"];
+ timerConfig.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.";
+ 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" ];
+ };
+ })
];
+ }))
- environment.etc."sr.ht/config.ini".source =
- settingsFormat.generate "sourcehut-config.ini"
- # Disabled services must be removed from the config.ini
- # to be effectively disabled.
- (filterAttrs (k: v:
- let srv = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" k; in
- srv == null || cfg.${head srv}.enable
- ) cfg.settings);
-
- environment.systemPackages = [ pkgs.sourcehut.coresrht ];
-
- # 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";
+ (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 [
+ {
+ 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';
+ '';
+ };
+ };
+ })
+ ];
+ })
+
+ (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}
+
+ 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
+
+ ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
+ # Just try all the migrations because they're not linked to the version
+ for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
+ ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
+ done
+ ''}
+
+ # Disable webhook
+ touch ${stateDir}/webhook
+ '';
+ serviceConfig = {
+ ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
+ };
+ };
+ })
+
+ (import ./service.nix "paste" {
+ inherit configIniOfService;
+ port = 5011;
+ })
+
+ (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 ];
}