Unverified Commit 43f7128b authored by Seth Flynn's avatar Seth Flynn Committed by GitHub
Browse files

nixos/reposilite: init module (#381197)

parents 230bd72d 3a856acf
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -75,6 +75,8 @@

- [MaryTTS](https://github.com/marytts/marytts), an open-source, multilingual text-to-speech synthesis system written in pure Java. Available as [services.marytts](options.html#opt-services.marytts).

- [Reposilite](https://reposilite.com), a lightweight and easy-to-use repository manager for Maven-based artifacts in the JVM ecosystem. Available as [services.reposilite](options.html#opt-services.reposilite).

- [networking.modemmanager](options.html#opt-networking.modemmanager) has been split out of [networking.networkmanager](options.html#opt-networking.networkmanager). NetworkManager still enables ModemManager by default, but options exist now to run NetworkManager without ModemManager.

- [Routinator 3000](https://nlnetlabs.nl/projects/routing/routinator/), a full-featured RPKI Relying Party software package that runs as a service which periodically downloads and verifies RPKI data.
+1 −0
Original line number Diff line number Diff line
@@ -1610,6 +1610,7 @@
  ./services/web-apps/pretix.nix
  ./services/web-apps/privatebin.nix
  ./services/web-apps/prosody-filer.nix
  ./services/web-apps/reposilite.nix
  ./services/web-apps/rimgo.nix
  ./services/web-apps/rutorrent.nix
  ./services/web-apps/screego.nix
+439 −0
Original line number Diff line number Diff line
{
  lib,
  config,
  pkgs,
  ...
}:
let
  cfg = config.services.reposilite;
  format = pkgs.formats.cdn { };
  configFile = format.generate "reposilite.cdn" cfg.settings;

  useEmbeddedDb = cfg.database.type == "sqlite" || cfg.database.type == "h2";
  useMySQL = cfg.database.type == "mariadb" || cfg.database.type == "mysql";
  usePostgres = cfg.database.type == "postgresql";

  # db password is appended at runtime by the service script (if needed)
  dbString =
    if useEmbeddedDb then
      "${cfg.database.type} ${cfg.database.path}"
    else
      "${cfg.database.type} ${cfg.database.host}:${builtins.toString cfg.database.port} ${cfg.database.dbname} ${cfg.database.user} $(<${cfg.database.passwordFile})";

  certDir = config.security.acme.certs.${cfg.useACMEHost}.directory;

  databaseModule = {
    options = {
      type = lib.mkOption {
        type = lib.types.enum [
          "h2"
          "mariadb"
          "mysql"
          "postgresql"
          "sqlite"
        ];
        description = ''
          Database engine to use.
        '';
        default = "sqlite";
      };

      path = lib.mkOption {
        type = lib.types.str;
        description = ''
          Path to the embedded database file. Set to `--temporary` to use an in-memory database.
        '';
        default = "reposilite.db";
      };

      host = lib.mkOption {
        type = lib.types.str;
        description = ''
          Database host address.
        '';
        default = "127.0.0.1";
      };

      port = lib.mkOption {
        type = lib.types.port;
        description = ''
          Database TCP port.
        '';
        defaultText = lib.literalExpression ''
          if type == "postgresql" then 5432 else 3306
        '';
        default = if usePostgres then config.services.postgresql.settings.port else 3306;
      };

      dbname = lib.mkOption {
        type = lib.types.str;
        description = ''
          Database name.
        '';
        default = "reposilite";
      };

      user = lib.mkOption {
        type = lib.types.str;
        description = ''
          Database user.
        '';
        default = "reposilite";
      };

      passwordFile = lib.mkOption {
        type = lib.types.nullOr lib.types.path;
        description = ''
          Path to the file containing the password for the database connection.
          This file must be readable by {option}`services.reposilite.user`.
        '';
        default = null;
      };
    };
  };

  settingsModule = {
    freeformType = format.type;
    options = {
      hostname = lib.mkOption {
        type = lib.types.str;
        description = ''
          The hostname to bind to. Set to `0.0.0.0` to accept connections from everywhere, or `127.0.0.1` to restrict to localhost."
        '';
        default = "0.0.0.0";
        example = "127.0.0.1";
      };

      port = lib.mkOption {
        type = lib.types.port;
        description = ''
          The TCP port to bind to.
        '';
        default = 3000;
      };

      database = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        description = ''
          Database connection string. Please use {option}`services.reposilite.database` instead.
          See https://reposilite.com/guide/general#local-configuration for valid values.
        '';
        default = null;
      };

      sslEnabled = lib.mkOption {
        type = lib.types.bool;
        description = ''
          Whether to listen for encrypted connections on {option}`settings.sslPort`.
        '';
        default = false;
      };

      sslPort = lib.mkOption {
        type = lib.types.port; # cant be null
        description = "SSL port to bind to. SSL needs to be enabled explicitly via {option}`settings.enableSsl`.";
        default = 443;
      };

      keyPath = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        description = ''
          Path to the .jsk KeyStore or paths to the PKCS#8 certificate and private key, separated by a space (see example).
          You can use `''${WORKING_DIRECTORY}` to refer to paths relative to Reposilite's working directory.
          If you are using a Java KeyStore, don't forget to specify the password via the {var}`REPOSILITE_LOCAL_KEYPASSWORD` environment variable.
          See https://reposilite.com/guide/ssl for more information on how to set SSL up.
        '';
        default = null;
        example = "\${WORKING_DIRECTORY}/cert.pem \${WORKING_DIRECTORY}/key.pem";
      };

      keyPassword = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        description = ''
          Plaintext password used to unlock the Java KeyStore set in {option}`services.reposilite.settings.keyPath`.
          WARNING: this option is insecure and should not be used to store the password.
          Consider using {option}`services.reposilite.keyPasswordFile` instead.
        '';
        default = null;
      };

      enforceSsl = lib.mkOption {
        type = lib.types.bool;
        description = ''
          Whether to redirect all traffic to SSL.
        '';
        default = false;
      };

      webThreadPool = lib.mkOption {
        type = lib.types.ints.between 5 65535;
        description = ''
          Maximum amount of threads used by the core thread pool. (min: 5)
          The web thread pool handles the first few steps of incoming HTTP connections, tasks are redirected as soon as possible to the IO thread pool.
        '';
        default = 16;
      };

      ioThreadPool = lib.mkOption {
        type = lib.types.ints.between 2 65535;
        description = ''
          The IO thread pool handles all tasks that may benefit from non-blocking IO. (min: 2)
          Because most tasks are redirected to IO thread pool, it might be a good idea to keep it at least equal to web thread pool.
        '';
        default = 8;
      };

      databaseThreadPool = lib.mkOption {
        type = lib.types.ints.positive;
        description = ''
          Maximum amount of concurrent connections to the database. (one per thread)
          Embedded databases (sqlite, h2) do not support truly concurrent connections, so the value will always be `1` if they are used.
        '';
        default = 1;
      };

      compressionStrategy = lib.mkOption {
        type = lib.types.enum [
          "none"
          "gzip"
        ];
        description = ''
          Compression algorithm used by this instance of Reposilite.
          `none` reduces usage of CPU & memory, but requires transfering more data.
        '';
        default = "none";
      };

      idleTimeout = lib.mkOption {
        type = lib.types.ints.unsigned;
        description = ''
          Default idle timeout used by Jetty.
        '';
        default = 30000;
      };

      bypassExternalCache = lib.mkOption {
        type = lib.types.bool;
        description = ''
          Add cache bypass headers to responses from /api/* to avoid issues with proxies such as Cloudflare.
        '';
        default = true;
      };

      cachedLogSize = lib.mkOption {
        type = lib.types.ints.unsigned;
        description = ''
          Amount of messages stored in the cache logger.
        '';
        default = 50;
      };

      defaultFrontend = lib.mkOption {
        type = lib.types.bool;
        description = ''
          Whether to enable the default included frontend with a dashboard.
        '';
        default = true;
      };

      basePath = lib.mkOption {
        type = lib.types.str;
        description = ''
          Custom base path for this Reposilite instance.
          It is not recommended changing this, you should instead prioritize using a different subdomain.
        '';
        default = "/";
      };

      debugEnabled = lib.mkOption {
        type = lib.types.bool;
        description = ''
          Whether to enable debug mode.
        '';
        default = false;
      };
    };
  };
in
{
  options.services.reposilite = {
    enable = lib.mkEnableOption "Reposilite";
    package = lib.mkPackageOption pkgs "reposilite" { } // {
      apply =
        pkg:
        pkg.override (old: {
          plugins = (old.plugins or [ ]) ++ cfg.plugins;
        });
    };

    plugins = lib.mkOption {
      type = lib.types.listOf lib.types.package;
      description = ''
        List of plugins to add to Reposilite.
      '';
      default = [ ];
      example = "with reposilitePlugins; [ checksum groovy ]";
    };

    database = lib.mkOption {
      description = "Database options.";
      default = { };
      type = lib.types.submodule databaseModule;
    };

    keyPasswordFile = lib.mkOption {
      type = lib.types.nullOr lib.types.path;
      description = ''
        Path the the file containing the password used to unlock the Java KeyStore file specified in {option}`services.reposilite.settings.keyPath`.
        This file must be readable my {option}`services.reposilite.user`.
      '';
      default = null;
    };

    useACMEHost = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      description = ''
        Host of an existing Let's Encrypt certificate to use for SSL.
        Make sure that the certificate directory is readable by the `reposilite` user or group, for example via {option}`security.acme.certs.<cert>.group`.
        *Note that this option does not create any certificates, nor it does add subdomains to existing ones – you will need to create them manually using {option}`security.acme.certs`*
      '';
      default = null;
    };

    settings = lib.mkOption {
      description = "Configuration written to the reposilite.cdn file";
      default = { };
      type = lib.types.submodule settingsModule;
    };

    workingDirectory = lib.mkOption {
      type = lib.types.path;
      description = ''
        Working directory for Reposilite.
      '';
      default = "/var/lib/reposilite";
    };

    extraArgs = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      description = ''
        Extra arguments/parameters passed to the Reposilite. Can be used for first token generation.
      '';
      default = [ ];
      example = lib.literalExpression ''[ "--token" "name:tempsecrettoken" ]'';
    };

    user = lib.mkOption {
      type = lib.types.str;
      description = ''
        The user to run Reposilite under.
      '';
      default = "reposilite";
    };

    group = lib.mkOption {
      type = lib.types.str;
      description = ''
        The group to run Reposilite under.
      '';
      default = "reposilite";
    };

    openFirewall = lib.mkOption {
      type = lib.types.bool;
      description = ''
        Whether to open the firewall ports for Reposilite. If SSL is enabled, its port will be opened too.
      '';
      default = false;
    };
  };

  config = lib.mkIf cfg.enable {
    assertions = [
      {
        assertion = cfg.settings.sslEnabled -> cfg.settings.keyPath != null;
        message = ''
          Reposilite was configured to enable SSL, but no valid paths to certificate files were provided via `settings.keyPath`.
          Read more about SSL certificates here: https://reposilite.com/guide/ssl
        '';
      }
      {
        assertion = cfg.settings.enforceSsl -> cfg.settings.sslEnabled;
        message = "You cannot enforce SSL if SSL is not enabled.";
      }
      {
        assertion = !useEmbeddedDb -> cfg.database.passwordFile != null;
        message = "You need to set `services.reposilite.database.passwordFile` when using MySQL or Postgres.";
      }
    ];

    services.reposilite.settings.keyPath = lib.mkIf (
      cfg.useACMEHost != null
    ) "${certDir}/fullchain.pem ${certDir}/key.pem";

    environment.systemPackages = [ cfg.package ];

    users = {
      groups.${cfg.group} = lib.mkIf (cfg.group == "reposilite") { };
      users.${cfg.user} = lib.mkIf (cfg.user == "reposilite") {
        isSystemUser = true;
        group = cfg.group;
      };
    };

    networking.firewall = lib.mkIf cfg.openFirewall (
      lib.mkMerge [
        {
          allowedTCPPorts = [ cfg.settings.port ];
        }
        (lib.mkIf cfg.settings.sslEnabled {
          allowedTCPPorts = [ cfg.settings.sslPort ];
        })
      ]
    );

    systemd.services.reposilite = {
      enable = true;
      wantedBy = [ "multi-user.target" ];
      after =
        [ "network.target" ]
        ++ (lib.optional useMySQL "mysql.service")
        ++ (lib.optional usePostgres "postgresql.service");

      script =
        lib.optionalString (cfg.keyPasswordFile != null && cfg.settings.keyPassword == null) ''
          export REPOSILITE_LOCAL_KEYPASSWORD="$(<${cfg.keyPasswordFile})"
        ''
        + ''
          export REPOSILITE_LOCAL_DATABASE="${dbString}"

          ${lib.getExe cfg.package} --local-configuration ${configFile} --local-configuration-mode none --working-directory ${cfg.workingDirectory} ${lib.escapeShellArgs cfg.extraArgs}
        '';

      serviceConfig = lib.mkMerge [
        (lib.mkIf (builtins.dirOf cfg.workingDirectory == "/var/lib") {
          StateDirectory = builtins.baseNameOf cfg.workingDirectory;
          StateDirectoryMode = "700";
        })
        {
          Type = "exec";
          Restart = "on-failure";

          User = cfg.user;
          Group = cfg.group;
          WorkingDirectory = cfg.workingDirectory;

          # TODO better hardening
          LimitNOFILE = "1048576";
          PrivateTmp = true;
          PrivateDevices = true;
          ProtectHome = true;
          ProtectSystem = "strict";
          AmbientCapabilities = "CAP_NET_BIND_SERVICE";
        }
      ];
    };
  };

  meta.maintainers = [ lib.maintainers.uku3lig ];
}
+1 −0
Original line number Diff line number Diff line
@@ -1148,6 +1148,7 @@ in
  redmine = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./redmine.nix { };
  renovate = handleTest ./renovate.nix { };
  replace-dependencies = handleTest ./replace-dependencies { };
  reposilite = runTest ./reposilite.nix;
  restartByActivationScript = handleTest ./restart-by-activation-script.nix { };
  restic-rest-server = handleTest ./restic-rest-server.nix { };
  restic = handleTest ./restic.nix { };
+53 −0
Original line number Diff line number Diff line
{ lib, ... }:
{
  name = "reposilite";

  nodes = {
    machine =
      { pkgs, ... }:
      {
        services = {
          mysql = {
            enable = true;
            package = pkgs.mariadb;
            ensureDatabases = [ "reposilite" ];
            initialScript = pkgs.writeText "reposilite-test-db-init" ''
              CREATE USER 'reposilite'@'localhost' IDENTIFIED BY 'ReposiliteDBPass';
              GRANT ALL PRIVILEGES ON reposilite.* TO 'reposilite'@'localhost';
              FLUSH PRIVILEGES;
            '';
          };

          reposilite = {
            enable = true;
            plugins = with pkgs.reposilitePlugins; [
              checksum
              groovy
            ];
            extraArgs = [
              "--token"
              "test:SuperSecretTestToken"
            ];
            database = {
              type = "mariadb";
              passwordFile = "/run/reposiliteDbPass";
            };
            settings.port = 8080;
          };
        };
      };
  };

  testScript = ''
    machine.start()

    machine.execute("echo \"ReposiliteDBPass\" > /run/reposiliteDbPass && chmod 600 /run/reposiliteDbPass && chown reposilite:reposilite /run/reposiliteDbPass")
    machine.wait_for_unit("reposilite.service")
    machine.wait_for_open_port(8080)

    machine.fail("curl -Sf localhost:8080/api/auth/me")
    machine.succeed("curl -Sfu test:SuperSecretTestToken localhost:8080/api/auth/me")
  '';

  meta.maintainers = [ lib.maintainers.uku3lig ];
}
Loading