Unverified Commit 63492318 authored by Masum Reza's avatar Masum Reza Committed by GitHub
Browse files

nixos/dolibarr: support PostgreSQL + H2O (#449173)

parents dd365723 9427b8ae
Loading
Loading
Loading
Loading
+292 −132
Original line number Diff line number Diff line
@@ -25,12 +25,19 @@ let
  package = cfg.package.override { inherit (cfg) stateDir; };

  cfg = config.services.dolibarr;
  vhostCfg = lib.optionalAttrs (cfg.nginx != null) config.services.nginx.virtualHosts."${cfg.domain}";

  forcedTLS =
    if cfg.h2o != null then
      cfg.h2o.tls != null && cfg.h2o.tls.policy == "force"
    else if cfg.nginx != null then
      cfg.nginx.forceSSL
    else
      false;

  mkConfigFile =
    filename: settings:
    let
      # hack in special logic for secrets so we read them from a separate file avoiding the nix store
      # hack in special logic for secrets so we read them from a separate file avoiding the Nix store
      secretKeys = [
        "force_install_databasepass"
        "dolibarr_main_db_pass"
@@ -55,6 +62,34 @@ let
      ${concatStringsSep "\n" (mapAttrsToList (k: v: "\$${k} = ${toStr k v};") settings)}
    '';

  dbUnit =
    {
      "mysql" = "mysql.service";
      "postgresql" = "postgresql.target";
    }
    .${cfg.database.type};

  dbPort =
    if cfg.database.createLocally then
      {
        "mysql" = config.services.mysql.settings.mysqld.port;
        "postgresql" = config.services.postgresql.settings.port;
      }
      .${cfg.database.type}
    else
      cfg.database.port;

  # exclusivity asserted in `assertions`
  webServerService =
    if cfg.h2o != null then
      "h2o.service"
    else if cfg.nginx != null then
      "nginx.service"
    else
      null;

  socketOwner = if cfg.h2o != null then config.services.h2o.user else cfg.user;

  # see https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/install/install.forced.sample.php for all possible values
  install = {
    force_install_noedit = 2;
@@ -62,13 +97,18 @@ let
    force_install_nophpinfo = true;
    force_install_lockinstall = "444";
    force_install_distrib = "nixos";
    force_install_type = "mysqli";
    force_install_type =
      {
        "mysql" = "mysqli";
        "postgresql" = "pgsql";
      }
      .${cfg.database.type};
    force_install_dbserver = cfg.database.host;
    force_install_port = toString cfg.database.port;
    force_install_port = toString dbPort;
    force_install_database = cfg.database.name;
    force_install_databaselogin = cfg.database.user;

    force_install_mainforcehttps = vhostCfg.forceSSL or false;
    force_install_mainforcehttps = forcedTLS;
    force_install_createuser = false;
    force_install_dolibarrlogin = null;
  }
@@ -128,6 +168,15 @@ in
    };

    database = {
      type = mkOption {
        type = types.enum [
          "mysql"
          "postgresql"
        ];
        example = "postgresql";
        default = "mysql";
        description = "Database engine to use.";
      };
      host = mkOption {
        type = types.str;
        default = "localhost";
@@ -173,6 +222,29 @@ in
      description = "Dolibarr settings, see <https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/conf/conf.php.example> for details.";
    };

    h2o = mkOption {
      type = types.nullOr (
        types.submodule (import ../web-servers/h2o/vhost-options.nix { inherit config lib; })
      );
      default = null;
      example =
        lib.literalExpression # nix
          ''
            {
              acme.enable = true;
              tls.policy = "force";
              compress = "ON";
            }
          '';
      description = ''
        With this option, you can customize an H2O virtual host which already
        has sensible defaults for Dolibarr. Set to `{ }` if you do not need any
        customization to the virtual host. If enabled, then by default, the
        {option}`serverName` is `''${domain}`, If this is set to `null` (the
        default), no H2O `hosts` will be configured.
      '';
    };

    nginx = mkOption {
      type = types.nullOr (
        types.submodule (
@@ -228,14 +300,29 @@ in
  };

  # implementation
  config = mkIf cfg.enable (mkMerge [
    {
  config = mkIf cfg.enable {

    assertions = [
      {
        assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
        message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned";
      }
      (
        let
          webServers = [
            "h2o"
            "nginx"
          ];
          checkConfigs = lib.concatMapStringsSep ", " (ws: "services.dolibarr.${ws}") webServers;
        in
        {
          assertion = builtins.length (lib.lists.filter (ws: cfg.${ws} != null) webServers) <= 1;
          message = ''
            At most 1 web server virtual host configuration should be enabled
            for Dolibarr at a time. Check ${checkConfigs}.
          '';
        }
      )
    ];

    services.dolibarr.settings = {
@@ -245,14 +332,19 @@ in
      dolibarr_main_data_root = "${cfg.stateDir}/documents";

      dolibarr_main_db_host = cfg.database.host;
        dolibarr_main_db_port = toString cfg.database.port;
      dolibarr_main_db_port = toString dbPort;
      dolibarr_main_db_name = cfg.database.name;
      dolibarr_main_db_prefix = "llx_";
      dolibarr_main_db_user = cfg.database.user;
      dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) ''
        file_get_contents("${cfg.database.passwordFile}")
      '';
        dolibarr_main_db_type = "mysqli";
      dolibarr_main_db_type =
        {
          "mysql" = "mysqli";
          "postgresql" = "pgsql";
        }
        .${cfg.database.type};
      dolibarr_main_db_character_set = mkDefault "utf8";
      dolibarr_main_db_collation = mkDefault "utf8_unicode_ci";

@@ -261,8 +353,17 @@ in

      # Security settings
      dolibarr_main_prod = true;
        dolibarr_main_force_https = vhostCfg.forceSSL or false;
        dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql";
      dolibarr_main_force_https = forcedTLS;
      dolibarr_main_restrict_os_commands =
        {
          "mysql" = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql";
          "postgresql" =
            let
              pkg = config.services.postgresql.package;
            in
            "${pkg}/bin/pg_dump, ${pkg}/bin/psql";
        }
        .${cfg.database.type};
      dolibarr_nocsrfcheck = false;
      dolibarr_main_instance_unique_id = ''
        file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id")
@@ -277,7 +378,7 @@ in
      "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}"
    ];

      services.mysql = mkIf cfg.database.createLocally {
    services.mysql = mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
      enable = mkDefault true;
      package = mkDefault pkgs.mariadb;
      ensureDatabases = [ cfg.database.name ];
@@ -291,6 +392,53 @@ in
      ];
    };

    services.postgresql = mkIf (cfg.database.createLocally && cfg.database.type == "postgresql") {
      enable = mkDefault true;
      ensureDatabases = [ cfg.database.name ];
      ensureUsers = [
        {
          name = cfg.database.user;
          ensureDBOwnership = true;
        }
      ];
      authentication = ''
        host ${cfg.database.name} ${cfg.database.user} localhost trust
      '';
    };

    services.h2o = mkIf (cfg.h2o != null) {
      enable = true;
      hosts."${cfg.domain}" = mkMerge [
        {
          settings = {
            paths = {
              "/" = {
                "file.dir" = "${package}/htdocs";
                "file.index" = [
                  "index.php"
                  "index.html"
                ];
                redirect = {
                  url = "/index.php/";
                  internal = "YES";
                  status = 307;
                };
              };
            };
            "file.custom-handler" = {
              extension = [ ".php" ];
              "fastcgi.document_root" = "${package}/htdocs";
              "fastcgi.connect" = {
                port = config.services.phpfpm.pools.dolibarr.socket;
                type = "unix";
              };
            };
          };
        }
        cfg.h2o
      ];
    };

    services.nginx.enable = mkIf (cfg.nginx != null) true;
    services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) (
      lib.mkMerge [
@@ -308,12 +456,18 @@ in
      ]
    );

      systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ];
    systemd.services."phpfpm-dolibarr" = {
      wantedBy = lib.optional (webServerService != null) webServerService;
      before = lib.optional (webServerService != null) webServerService;
      after = lib.optional cfg.database.createLocally dbUnit;
      requires = lib.optional cfg.database.createLocally dbUnit;
    };

    services.phpfpm.pools.dolibarr = {
      inherit (cfg) user group;
      phpPackage = pkgs.php83.buildEnv {
        extensions = { enabled, all }: enabled ++ [ all.calendar ];
          # recommended by dolibarr web application
        # recommended by Dolibarr web application
        extraConfig = ''
          session.use_strict_mode = 1
          session.cookie_samesite = "Lax"
@@ -325,19 +479,20 @@ in

      settings = {
        "listen.mode" = "0660";
          "listen.owner" = cfg.user;
        "listen.owner" = socketOwner;
        "listen.group" = cfg.group;
      }
      // cfg.poolConfig;
    };

      # there are several challenges with dolibarr and NixOS which we can address here
      # - the dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php
      # - the dolibarr installer requires write access to its config file during installation, though not afterwards
      # - the dolibarr config file generally holds secrets generated by the installer, though the config file is a php file so we can read and write these secrets from an external file
    # There are several challenges with Dolibarr and NixOS which we can address here
    # - the Dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php
    # - the Dolibarr installer requires write access to its config file during installation, though not afterwards
    # - the Dolibarr config file generally holds secrets generated by the installer, though the config file is a PHP file so we can read and write these secrets from an external file
    systemd.services.dolibarr-config = {
      description = "dolibarr configuration file management via NixOS";
      wantedBy = [ "multi-user.target" ];
      after = lib.optional cfg.database.createLocally dbUnit;

      script =
        let
@@ -363,17 +518,22 @@ in
      };
    };

      users.users.dolibarr = mkIf (cfg.user == "dolibarr") {
    users = {
      users = {
        dolibarr = mkIf (cfg.user == "dolibarr") {
          isSystemUser = true;
          group = cfg.group;
        };

      users.groups = optionalAttrs (cfg.group == "dolibarr") {
      }
      // lib.optionalAttrs (cfg.h2o != null) {
        "${config.services.h2o.user}".extraGroups = [ cfg.group ];
      }
      // lib.optionalAttrs (cfg.nginx != null) {
        "${config.services.nginx.user}".extraGroups = [ cfg.group ];
      };
      groups = optionalAttrs (cfg.group == "dolibarr") {
        dolibarr = { };
      };
    }
    (mkIf (cfg.nginx != null) {
      users.users."${config.services.nginx.group}".extraGroups = mkIf (cfg.nginx != null) [ cfg.group ];
    })
  ]);
    };
  };
}
+77 −51
Original line number Diff line number Diff line
@@ -3,7 +3,8 @@
  name = "dolibarr";
  meta.maintainers = [ ];

  nodes.machine =
  nodes = {
    nginx_mysql =
      { ... }:
      {
        services.dolibarr = {
@@ -13,12 +14,29 @@
            forceSSL = false;
            enableACME = false;
          };
          database.type = "mysql";
        };

        networking.firewall.allowedTCPPorts = [ 80 ];
      };
    h2o_postgresql =
      { ... }:
      {
        services.dolibarr = {
          enable = true;
          domain = "localhost";
          h2o = {
            acme.enable = false;
          };
          database.type = "postgresql";
        };

  testScript = ''
        networking.firewall.allowedTCPPorts = [ 80 ];
      };
  };

  testScript = # python
    ''
      from html.parser import HTMLParser
      start_all()

@@ -32,12 +50,20 @@
        def handle_endtag(self, tag): pass
        def handle_data(self, data): pass

      # wait for app
      for machine in (nginx_mysql, h2o_postgresql):
        machine.wait_for_unit("phpfpm-dolibarr.service")
    machine.wait_for_unit("nginx.service")
    machine.wait_for_open_port(80)

      # wait for web servers
      nginx_mysql.wait_for_unit("nginx.service")
      nginx_mysql.wait_for_open_port(80)
      h2o_postgresql.wait_for_unit("h2o.service")
      h2o_postgresql.wait_for_open_port(80)

      for machine in (nginx_mysql, h2o_postgresql):
        # Sanity checks on URLs.
    # machine.succeed("curl -fL http://localhost/index.php")
    # machine.succeed("curl -fL http://localhost/")
        machine.succeed("curl -fL http://localhost/index.php")
        machine.succeed("curl -fL http://localhost/")
        # Perform installation.
        machine.succeed('curl -fL -X POST http://localhost/install/check.php -F selectlang=auto')
        machine.succeed('curl -fL -X POST http://localhost/install/fileconf.php -F selectlang=auto')