{ config, lib, pkgs, ... }: with lib; let cfg = config.fudo.services.gitea-container; hostname = config.instance.hostname; domainName = config.fudo.hosts."${hostname}".domain; zoneName = config.fudo.domains."${domainName}".zone; siteName = config.fudo.hosts."${hostname}".site; getSiteGatewayV4 = pkgs.lib.getSiteGatewayV4; keyPaths = { ed25519 = "/state/ssh/ed25519.key"; ecdsa = "/state/ssh/ecdsa.key"; }; in { options.fudo.services.gitea-container = with types; { enable = mkEnableOption "Enable Gitea running in a container."; site-name = mkOption { type = str; description = "Name of this Gitea instance."; }; hostname = mkOption { type = str; description = "Hostname at which the server is reachable."; }; state-directory = mkOption { type = str; description = "Path at which to store Gitea state."; }; trusted-networks = mkOption { type = listOf str; description = "List of networks to be considered trusted (for metrics access)."; default = [ ]; }; networking = { interface = mkOption { type = str; description = "Parent host interface on which to listen."; }; ipv4 = mkOption { type = nullOr (submodule { options = { address = mkOption { type = str; description = "IP address."; }; prefixLength = mkOption { type = int; description = "Significant bits in the address."; }; }; }); default = null; }; ipv6 = mkOption { type = nullOr (submodule { options = { address = mkOption { type = str; description = "IP address."; }; prefixLength = mkOption { type = int; description = "Significant bits in the address."; }; }; }); default = null; }; }; }; config = mkIf cfg.enable { systemd.tmpfiles.rules = [ "d ${cfg.state-directory}/gitea 755 root root - -" "d ${cfg.state-directory}/acme 755 root root - -" ]; containers.gitea = { autoStart = true; additionalCapabilities = [ "CAP_NET_ADMIN" ]; macvlans = [ cfg.networking.interface ]; bindMounts = { "/state" = { hostPath = "${cfg.state-directory}/gitea"; isReadOnly = false; }; "/var/lib/acme" = { hostPath = "${cfg.state-directory}/acme"; isReadOnly = false; }; }; config = { nixpkgs.pkgs = pkgs; systemd = { tmpfiles.rules = [ "d /state 0755 root root - -" ]; services = { gitea-chown = { requiredBy = [ "gitea.service" ]; before = [ "gitea.service" ]; serviceConfig.Type = "oneshot"; script = "chown -R gitea:gitea /state"; }; gitea-keygen = { requiredBy = [ "gitea.service" ]; before = [ "gitea.service" ]; serviceConfig.Type = "oneshot"; script = let keygenScripts = mapAttrsToList (type: path: let dir = dirOf path; in '' if [ ! -f ${path} ]; then mkdir -p ${dir} ${pkgs.openssh}/bin/ssh-keygen -q -N "" -t ${type} -f ${path} chown -R gitea:gitea ${dir} chmod 0750 ${dir} chmod 0440 ${path} fi '') keyPaths; in concatStringsSep "\n" keygenScripts; }; }; }; environment.systemPackages = let giteaCli = pkgs.writeShellApplication { name = "gitea-cli"; runtimeInputs = with pkgs; [ gitea ]; text = ''gitea --config /state/gitea/custom/conf/app.ini "$@"''; }; in [ giteaCli ]; networking = { defaultGateway = { address = getSiteGatewayV4 siteName; interface = "mv-${cfg.networking.interface}"; }; enableIPv6 = !isNull cfg.networking.ipv6; nameservers = config.networking.nameservers; firewall = { enable = true; allowedTCPPorts = [ 22 80 443 ]; }; interfaces."mv-${cfg.networking.interface}" = { ipv4.addresses = optional (!isNull cfg.networking.ipv4) { address = cfg.networking.ipv4.address; prefixLength = cfg.networking.ipv4.prefixLength; }; ipv6.addresses = optional (!isNull cfg.networking.ipv6) { address = cfg.networking.ipv6.address; prefixLength = cfg.networking.ipv6.prefixLength; }; }; }; security.acme = { acceptTerms = true; defaults.email = "admin@${cfg.hostname}"; }; services = { gitea = { enable = true; appName = cfg.site-name; database = { createDatabase = true; type = "sqlite3"; }; repositoryRoot = "/state/repositories"; stateDir = "/state/gitea"; settings = { service = { #DISABLE_REGISTRATION = true; ALLOW_ONLY_EXTERNAL_REGISTRATION = true; }; security = { INSTALL_LOCK = true; LOGIN_REMEMBER_DAYS = 30; }; metrics.ENABLED = cfg.trusted-networks != [ ]; server = { START_SSH_SERVER = true; # Host & port to display in the clone URL SSH_DOMAIN = cfg.hostname; # SSH_LISTEN_HOST = "0.0.0.0"; SSH_PORT = 22; SSH_LISTEN_PORT = 2222; DOMAIN = cfg.hostname; ROOT_URL = "https://${cfg.hostname}"; SSH_SERVER_HOST_KEYS = concatStringsSep "," (attrValues keyPaths); HTTP_ADDR = "127.0.0.1"; HTTP_PORT = 8080; }; oauth2_client = { REGISTER_EMAIL_CONFIRM = false; OPENID_CONNECT_SCOPES = concatStringsSep "," [ "email" "profile" ]; #ENABLE_AUTO_REGISTRATION = true; USERNAME = "email"; UPDATE_AVATAR = true; ACCOUNT_LINKING = "login"; }; "git.timeout" = { DEFAULT = 720; MIGRATE = 3600; MIRROR = 3600; CLONE = 1200; PULL = 1200; GC = 600; }; }; }; xinetd = { enable = true; services = [{ name = "ssh"; port = 22; protocol = "tcp"; extraConfig = '' redirect = localhost 2222 wait = no socket_type = stream ''; user = "nobody"; # Must be defined, but not used server = "/usr/bin/env"; # unlisted = true; }]; }; nginx = { enable = true; recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; recommendedGzipSettings = true; virtualHosts."${cfg.hostname}" = { enableACME = true; forceSSL = true; locations."/" = { proxyPass = "http://127.0.0.1:8080"; proxyWebsockets = true; recommendedProxySettings = true; }; locations."/metrics" = mkIf (cfg.trusted-networks != [ ]) { proxyPass = "http://127.0.0.1:8080/metrics"; extraConfig = let networkAllowClauses = map (net: "allow ${net};") cfg.trusted-networks; in concatStringsSep "\n" (networkAllowClauses ++ [ "deny all;" ]); }; }; }; }; }; }; }; }