Initial implementation: hello and postgres

This commit is contained in:
Sridhar Ratnakumar 2023-06-19 17:54:46 -04:00 committed by Sridhar Ratnakumar
parent f156b16946
commit f171932d13
9 changed files with 480 additions and 1 deletions

View File

@ -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.

10
flake.nix Normal file
View File

@ -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"; };
};
};
}

6
nix/default.nix Normal file
View File

@ -0,0 +1,6 @@
{
imports = [
./hello.nix
./postgres.nix
];
}

24
nix/hello.nix Normal file
View File

@ -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}"
'';
};
}

276
nix/postgres.nix Normal file
View File

@ -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
<https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
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";
};
};
};
}

12
nix/postgres_test.nix Normal file
View File

@ -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")
'';
}

9
test.sh Executable file
View File

@ -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

96
test/flake.lock Normal file
View File

@ -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
}

25
test/flake.nix Normal file
View File

@ -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
];
};
};
};
};
}