diff --git a/README.md b/README.md index 5097791..859d804 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # services-flake -NixOS-like services for Nix flakes + +NixOS-like services for Nix flakes, as a [process-compose-flake](https://github.com/Platonic-Systems/process-compose-flake) module (based on flake-parts). + +## Getting Started + +TODO + +## Services available + +- [x] Hello World +- [-] PostgreSQL +- [ ] MySQL +- [ ] Redis +- [ ] ... + +## Contributing + +We do not have CI yet, so please run `./test.sh` on **NixOS**. + +## Credits + +Thanks to [the devenv project](https://github.com/cachix/devenv/tree/main/src/modules/services) on which much of our services implementation is based on. \ No newline at end of file diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a0fc748 --- /dev/null +++ b/flake.nix @@ -0,0 +1,10 @@ +{ + outputs = _: { + processComposeModules.default = ./nix; + + templates.default = { + description = "Example flake using process-compose-flake"; + path = builtins.path { path = ./example; filter = path: _: baseNameOf path == "flake.nix"; }; + }; + }; +} diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 0000000..40f3bea --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./hello.nix + ./postgres.nix + ]; +} diff --git a/nix/hello.nix b/nix/hello.nix new file mode 100644 index 0000000..068afcc --- /dev/null +++ b/nix/hello.nix @@ -0,0 +1,24 @@ +{ pkgs, lib, config, ... }: + +{ + options.services.hello = { + enable = lib.mkEnableOption "hello"; + name = lib.mkOption { + type = lib.types.str; + default = "hello"; + description = "Process name"; + }; + package = lib.mkPackageOption pkgs "hello" { }; + greeting = lib.mkOption { + type = lib.types.str; + default = "Hello"; + description = "The greeting to use"; + }; + }; + config = let cfg = config.services.hello; in lib.mkIf cfg.enable { + settings.processes.${cfg.name}.command = '' + set -x + ${lib.getExe cfg.package} -g "${cfg.greeting}" + ''; + }; +} diff --git a/nix/postgres.nix b/nix/postgres.nix new file mode 100644 index 0000000..d3af6ab --- /dev/null +++ b/nix/postgres.nix @@ -0,0 +1,276 @@ +# Based on https://github.com/cachix/devenv/blob/main/src/modules/services/postgres.nix +{ pkgs, lib, config, ... }: + +let + inherit (lib) types; +in +{ + options.services.postgres = lib.mkOption { + description = '' + Enable postgresql server + ''; + default = { }; + type = lib.types.submodule ({ config, ... }: { + options = { + enable = lib.mkEnableOption "postgres"; + name = lib.mkOption { + type = lib.types.str; + default = "postgres"; + description = "Unique process name"; + }; + + package = lib.mkPackageOption pkgs "postgresql" { }; + extensions = lib.mkOption { + type = with types; nullOr (functionTo (listOf package)); + default = null; + example = lib.literalExpression '' + extensions: [ + extensions.pg_cron + extensions.postgis + extensions.timescaledb + ]; + ''; + description = '' + Additional PostgreSQL extensions to install. + + The available extensions are: + + ${lib.concatLines (builtins.map (x: "- " + x) (builtins.attrNames pkgs.postgresql.pkgs))} + ''; + }; + + dataDir = lib.mkOption { + type = lib.types.str; + default = "./data/${config.name}"; + description = "The DB data directory"; + }; + listen_addresses = lib.mkOption { + type = lib.types.str; + description = "Listen address"; + default = ""; + example = "127.0.0.1"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 5432; + description = '' + The TCP port to accept connections. + ''; + }; + + createDatabase = lib.mkOption { + type = types.bool; + default = true; + description = '' + Create a database named like current user on startup. Only applies when initialDatabases is an empty list. + ''; + }; + + initdbArgs = lib.mkOption { + type = types.listOf types.lines; + default = [ "--locale=C" "--encoding=UTF8" ]; + example = [ "--data-checksums" "--allow-group-access" ]; + description = '' + Additional arguments passed to `initdb` during data dir + initialisation. + ''; + }; + + settings = lib.mkOption { + type = with lib.types; attrsOf (oneOf [ bool float int str ]); + default = { }; + description = '' + PostgreSQL configuration. Refer to + + for an overview of `postgresql.conf`. + + String values will automatically be enclosed in single quotes. Single quotes will be + escaped with two single quotes as described by the upstream documentation linked above. + ''; + default = { + listen_addresses = config.listen_addresses; + port = config.port; + unix_socket_directories = lib.mkDefault config.dataDir; + }; + example = lib.literalExpression '' + { + log_connections = true; + log_statement = "all"; + logging_collector = true + log_disconnections = true + log_destination = lib.mkForce "syslog"; + } + ''; + }; + + initialDatabases = lib.mkOption { + type = types.listOf (types.submodule { + options = { + name = lib.mkOption { + type = types.str; + description = '' + The name of the database to create. + ''; + }; + schema = lib.mkOption { + type = types.nullOr types.path; + default = null; + description = '' + The initial schema of the database; if null (the default), + an empty database is created. + ''; + }; + }; + }); + default = [ ]; + description = '' + List of database names and their initial schemas that should be used to create databases on the first startup + of Postgres. The schema attribute is optional: If not specified, an empty database is created. + ''; + example = lib.literalExpression '' + [ + { + name = "foodatabase"; + schema = ./foodatabase.sql; + } + { name = "bardatabase"; } + ] + ''; + }; + + initialScript = lib.mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Initial SQL commands to run during database initialization. This can be multiple + SQL expressions separated by a semi-colon. + ''; + example = lib.literalExpression '' + CREATE USER postgres SUPERUSER; + CREATE USER bar; + ''; + }; + }; + }); + }; + config = let cfg = config.services.postgres; in lib.mkIf cfg.enable { + settings.processes = + let + postgresPkg = + if cfg.extensions != null then + if builtins.hasAttr "withPackages" cfg.package + then cfg.package.withPackages cfg.extensions + else + builtins.throw '' + Cannot add extensions to the PostgreSQL package. + `services.postgres.package` is missing the `withPackages` attribute. Did you already add extensions to the package? + '' + else cfg.package; + in + { + # DB initialization + "${cfg.name}-init".command = + let + setupInitialDatabases = + if cfg.initialDatabases != [ ] then + (lib.concatMapStrings + (database: '' + echo "Checking presence of database: ${database.name}" + # Create initial databases + dbAlreadyExists="$( + echo "SELECT 1 as exists FROM pg_database WHERE datname = '${database.name}';" | \ + postgres --single -E postgres | \ + ${pkgs.gnugrep}/bin/grep -c 'exists = "1"' || true + )" + echo $dbAlreadyExists + if [ 1 -ne "$dbAlreadyExists" ]; then + echo "Creating database: ${database.name}" + echo 'create database "${database.name}";' | postgres --single -E postgres + + + ${lib.optionalString (database.schema != null) '' + echo "Applying database schema on ${database.name}" + if [ -f "${database.schema}" ] + then + echo "Running file ${database.schema}" + ${pkgs.gawk}/bin/awk 'NF' "${database.schema}" | postgres --single -j -E ${database.name} + elif [ -d "${database.schema}" ] + then + # Read sql files in version order. Apply one file + # at a time to handle files where the last statement + # doesn't end in a ;. + ls -1v "${database.schema}"/*.sql | while read f ; do + echo "Applying sql file: $f" + ${pkgs.gawk}/bin/awk 'NF' "$f" | postgres --single -j -E ${database.name} + done + else + echo "ERROR: Could not determine how to apply schema with ${database.schema}" + exit 1 + fi + ''} + fi + '') + cfg.initialDatabases) + else + lib.optionalString cfg.createDatabase '' + echo "CREATE DATABASE ''${USER:-$(id -nu)};" | postgres --single -E postgres ''; + + runInitialScript = + if cfg.initialScript != null then + '' + echo "${cfg.initialScript}" | postgres --single -E postgres + '' + else + ""; + + toStr = value: + if true == value then + "yes" + else if false == value then + "no" + else if lib.isString value then + "'${lib.replaceStrings [ "'" ] [ "''" ] value}'" + else + toString value; + + configFile = pkgs.writeText "postgresql.conf" (lib.concatStringsSep "\n" + (lib.mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings)); + + setupScript = pkgs.writeShellScriptBin "setup-postgres" '' + set -euo pipefail + export PATH=${postgresPkg}/bin:${pkgs.coreutils}/bin + + if [[ ! -d "$PGDATA" ]]; then + initdb ${lib.concatStringsSep " " cfg.initdbArgs} + ${setupInitialDatabases} + + ${runInitialScript} + fi + + # Setup config + cp ${configFile} "$PGDATA/postgresql.conf" + ''; + in + '' + export PGDATA="${cfg.dataDir}" + ${lib.getExe setupScript} + ''; + + # DB process + ${cfg.name} = { + command = '' + set -x + export PATH="${postgresPkg}"/bin:$PATH + export LOCKDIR="/tmp" + postgres -k $LOCKDIR -D ${cfg.dataDir} + ''; + depends_on."${cfg.name}-init".condition = "process_completed_successfully"; + # SIGINT (= 2) for faster shutdown: https://www.postgresql.org/docs/current/server-shutdown.html + shutdown.signal = 2; + # https://github.com/F1bonacc1/process-compose#-auto-restart-if-not-healthy + availability.restart = "on_failure"; + }; + }; + }; +} diff --git a/nix/postgres_test.nix b/nix/postgres_test.nix new file mode 100644 index 0000000..b2070e1 --- /dev/null +++ b/nix/postgres_test.nix @@ -0,0 +1,12 @@ +{ config, ... }: { + services.postgres = { + enable = true; + listen_addresses = "127.0.0.1"; + }; + testScript = '' + process_compose.wait_until(lambda procs: + procs["postgres"]["status"] == "Running" + ) + machine.succeed("echo 'SELECT version();' | ${config.services.postgres.package}/bin/psql -h 127.0.0.1 -U tester") + ''; +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..3aba908 --- /dev/null +++ b/test.sh @@ -0,0 +1,9 @@ +set -euxo pipefail + +cd "$(dirname "$0")" + +# On NixOS, run the VM tests to test runtime behaviour +if command -v nixos-rebuild &> /dev/null; then + # service tests + nix flake check -L ./test +fi diff --git a/test/flake.lock b/test/flake.lock new file mode 100644 index 0000000..029bc76 --- /dev/null +++ b/test/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1685662779, + "narHash": "sha256-cKDDciXGpMEjP1n6HlzKinN0H+oLmNpgeCTzYnsA2po=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "71fb97f0d875fd4de4994dfb849f2c75e17eb6c3", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1686089707, + "narHash": "sha256-LTNlJcru2qJ0XhlhG9Acp5KyjB774Pza3tRH0pKIb3o=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "af21c31b2a1ec5d361ed8050edd0303c31306397", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1685564631, + "narHash": "sha256-8ywr3AkblY4++3lIVxmrWZFzac7+f32ZEhH/A8pNscI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4f53efe34b3a8877ac923b9350c874e3dcd5dc0a", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "process-compose-flake": { + "locked": { + "lastModified": 1687194805, + "narHash": "sha256-DAqMJFWXft+eWrPOLWrP4xdK1DQELWkkU47PM+ODG2U=", + "owner": "Platonic-Systems", + "repo": "process-compose-flake", + "rev": "dd1dff00700deb2bc4afa44e7082c0176c095fd9", + "type": "github" + }, + "original": { + "owner": "Platonic-Systems", + "repo": "process-compose-flake", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "process-compose-flake": "process-compose-flake", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/test/flake.nix b/test/flake.nix new file mode 100644 index 0000000..a6a2813 --- /dev/null +++ b/test/flake.nix @@ -0,0 +1,25 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + systems.url = "github:nix-systems/default"; + process-compose-flake.url = "github:Platonic-Systems/process-compose-flake"; + }; + outputs = inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = import inputs.systems; + imports = [ + inputs.process-compose-flake.flakeModule + ]; + perSystem = { self', pkgs, lib, ... }: { + process-compose = { + postgres = { + imports = [ + inputs.process-compose-flake.processComposeModules.services + ../nix/postgres_test.nix + ]; + }; + }; + }; + }; +}