diff --git a/ansible/tasks/setup-docker.yml b/ansible/tasks/setup-docker.yml deleted file mode 100644 index 7b37f70cc..000000000 --- a/ansible/tasks/setup-docker.yml +++ /dev/null @@ -1,80 +0,0 @@ -- name: Copy extension packages - copy: - src: files/extensions/ - dest: /tmp/extensions/ - when: debpkg_mode - -# Builtin apt module does not support wildcard for deb paths -- name: Install extensions - shell: | - set -e - apt-get update - apt-get install -y --no-install-recommends /tmp/extensions/*.deb - when: debpkg_mode - -- name: pgsodium - determine postgres bin directory - shell: pg_config --bindir - register: pg_bindir_output - when: debpkg_mode - -- set_fact: - pg_bindir: "{{ pg_bindir_output.stdout }}" - when: debpkg_mode - -- name: pgsodium - set pgsodium.getkey_script - become: yes - lineinfile: - path: /etc/postgresql/postgresql.conf - state: present - # script is expected to be placed by finalization tasks for different target platforms - line: pgsodium.getkey_script= '{{ pg_bindir }}/pgsodium_getkey.sh' - when: debpkg_mode - -# supautils -- name: supautils - add supautils to session_preload_libraries - become: yes - replace: - path: /etc/postgresql/postgresql.conf - regexp: "#session_preload_libraries = ''" - replace: session_preload_libraries = 'supautils' - when: debpkg_mode or stage2_nix - -- name: supautils - write custom supautils.conf - template: - src: "files/postgresql_config/supautils.conf.j2" - dest: /etc/postgresql-custom/supautils.conf - mode: 0664 - owner: postgres - group: postgres - when: debpkg_mode or stage2_nix - -- name: supautils - copy extension custom scripts - copy: - src: files/postgresql_extension_custom_scripts/ - dest: /etc/postgresql-custom/extension-custom-scripts - become: yes - when: debpkg_mode or stage2_nix - -- name: supautils - chown extension custom scripts - file: - mode: 0775 - owner: postgres - group: postgres - path: /etc/postgresql-custom/extension-custom-scripts - recurse: yes - become: yes - when: debpkg_mode or stage2_nix - -- name: supautils - include /etc/postgresql-custom/supautils.conf in postgresql.conf - become: yes - replace: - path: /etc/postgresql/postgresql.conf - regexp: "#include = '/etc/postgresql-custom/supautils.conf'" - replace: "include = '/etc/postgresql-custom/supautils.conf'" - when: debpkg_mode or stage2_nix - -- name: Cleanup - extension packages - file: - path: /tmp/extensions - state: absent - when: debpkg_mode diff --git a/ansible/tasks/setup-postgres.yml b/ansible/tasks/setup-postgres.yml index 2fe302488..1a9b1a3f8 100644 --- a/ansible/tasks/setup-postgres.yml +++ b/ansible/tasks/setup-postgres.yml @@ -1,123 +1,3 @@ -- name: Postgres - copy package - copy: - src: files/postgres/ - dest: /tmp/build/ - when: debpkg_mode - -- name: Postgres - add PPA - apt_repository: - repo: "deb [ trusted=yes ] file:///tmp/build ./" - state: present - when: debpkg_mode - -- name: Postgres - install commons - apt: - name: postgresql-common - install_recommends: no - when: debpkg_mode - -- name: Do not create main cluster - shell: - cmd: sed -ri 's/#(create_main_cluster) .*$/\1 = false/' /etc/postgresql-common/createcluster.conf - when: debpkg_mode - -- name: Postgres - install server - apt: - name: postgresql-{{ postgresql_major }}={{ postgresql_release }}-1.pgdg24.04+1 - install_recommends: no - when: debpkg_mode - -- name: Postgres - remove PPA - apt_repository: - repo: "deb [ trusted=yes ] file:///tmp/build ./" - state: absent - when: debpkg_mode - -- name: Postgres - cleanup package - file: - path: /tmp/build - state: absent - when: debpkg_mode - -- name: install locales - apt: - name: locales - state: present - become: yes - when: stage2_nix - -- name: configure locales - command: echo "C.UTF-8 UTF-8" > /etc/locale.gen && echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen - become: yes - when: stage2_nix - -- name: locale-gen - command: sudo locale-gen - when: stage2_nix - -- name: update-locale - command: sudo update-locale - when: stage2_nix - -- name: Create symlink to /usr/lib/postgresql/bin - shell: - cmd: ln -s /usr/lib/postgresql/{{ postgresql_major }}/bin /usr/lib/postgresql/bin - when: debpkg_mode - -- name: create ssl-cert group - group: - name: ssl-cert - state: present - when: nixpkg_mode -# the old method of installing from debian creates this group, but we must create it explicitly -# for the nix built version - -- name: create postgres group - group: - name: postgres - state: present - when: nixpkg_mode - -- name: create postgres user - shell: adduser --system --home /var/lib/postgresql --no-create-home --shell /bin/bash --group --gecos "PostgreSQL administrator" postgres - args: - executable: /bin/bash - become: yes - when: nixpkg_mode - -- name: add postgres user to postgres group - shell: usermod -a -G ssl-cert postgres - args: - executable: /bin/bash - become: yes - when: nixpkg_mode - -- name: Create relevant directories - file: - path: '{{ item }}' - recurse: yes - state: directory - owner: postgres - group: postgres - with_items: - - '/home/postgres' - - '/var/log/postgresql' - - '/var/lib/postgresql' - when: debpkg_mode or nixpkg_mode - -- name: Allow adminapi to write custom config - file: - path: '{{ item }}' - recurse: yes - state: directory - owner: postgres - group: postgres - mode: 0775 - with_items: - - '/etc/postgresql' - - '/etc/postgresql-custom' - when: debpkg_mode or nixpkg_mode - - name: create placeholder config files file: path: '/etc/postgresql-custom/{{ item }}' @@ -130,193 +10,13 @@ - 'custom-overrides.conf' when: debpkg_mode or nixpkg_mode -# Move Postgres configuration files into /etc/postgresql -# Add postgresql.conf -- name: import postgresql.conf - template: - src: files/postgresql_config/postgresql.conf.j2 - dest: /etc/postgresql/postgresql.conf - group: postgres - when: debpkg_mode or nixpkg_mode - -# Add pg_hba.conf -- name: import pg_hba.conf - template: - src: files/postgresql_config/pg_hba.conf.j2 - dest: /etc/postgresql/pg_hba.conf - group: postgres - when: debpkg_mode or nixpkg_mode - -# Add pg_ident.conf -- name: import pg_ident.conf - template: - src: files/postgresql_config/pg_ident.conf.j2 - dest: /etc/postgresql/pg_ident.conf - group: postgres - when: debpkg_mode or nixpkg_mode - -# Add custom config for read replicas set up -- name: Move custom read-replica.conf file to /etc/postgresql-custom/read-replica.conf - template: - src: "files/postgresql_config/custom_read_replica.conf.j2" - dest: /etc/postgresql-custom/read-replica.conf - mode: 0664 - owner: postgres - group: postgres - when: debpkg_mode or nixpkg_mode - -# Install extensions before init -- name: Install Postgres extensions - import_tasks: tasks/setup-docker.yml - when: debpkg_mode or stage2_nix - #stage 2 postgres tasks - name: stage2 postgres tasks import_tasks: tasks/stage2-setup-postgres.yml when: stage2_nix -# init DB -- name: Create directory on data volume - file: - path: '{{ item }}' - recurse: yes - state: directory - owner: postgres - group: postgres - mode: 0750 - with_items: - - "/data/pgdata" - when: debpkg_mode or nixpkg_mode - -- name: Link database data_dir to data volume directory - file: - src: "/data/pgdata" - path: "/var/lib/postgresql/data" - state: link - force: yes - when: debpkg_mode or nixpkg_mode - -- name: Initialize the database - become: yes - become_user: postgres - shell: /usr/lib/postgresql/bin/pg_ctl -D /var/lib/postgresql/data initdb -o "--allow-group-access" -o "--username=supabase_admin" - vars: - ansible_command_timeout: 60 - when: debpkg_mode - -- name: Make sure .bashrc exists - file: - path: /var/lib/postgresql/.bashrc - state: touch - owner: postgres - group: postgres - when: nixpkg_mode - -- name: Check psql_version and modify supautils.conf and postgresql.conf if necessary - block: - - name: Check if psql_version is psql_orioledb - set_fact: - is_psql_oriole: "{{ psql_version in ['psql_orioledb-17'] }}" - is_psql_17: "{{ psql_version in ['psql_17'] }}" - - - name: Initialize the database stage2_nix (non-orioledb) - become: yes - become_user: postgres - shell: source /var/lib/postgresql/.bashrc && /usr/lib/postgresql/bin/pg_ctl -D /var/lib/postgresql/data initdb -o "--allow-group-access" -o "--username=supabase_admin" - args: - executable: /bin/bash - environment: - LANG: en_US.UTF-8 - LANGUAGE: en_US.UTF-8 - LC_ALL: en_US.UTF-8 - LC_CTYPE: en_US.UTF-8 - LOCALE_ARCHIVE: /usr/lib/locale/locale-archive - vars: - ansible_command_timeout: 60 - when: stage2_nix and not is_psql_oriole and not is_psql_17 - - - name: Initialize the database stage2_nix (orioledb) - become: yes - become_user: postgres - shell: > - source /var/lib/postgresql/.bashrc && /usr/lib/postgresql/bin/pg_ctl -D /var/lib/postgresql/data initdb - -o "--allow-group-access" - -o "--username=supabase_admin" - -o "--locale-provider=icu" - -o "--encoding=UTF-8" - -o "--icu-locale=en_US.UTF-8" - args: - executable: /bin/bash - environment: - LANG: en_US.UTF-8 - LANGUAGE: en_US.UTF-8 - LC_ALL: en_US.UTF-8 - LC_CTYPE: en_US.UTF-8 - LOCALE_ARCHIVE: /usr/lib/locale/locale-archive - vars: - ansible_command_timeout: 60 - when: stage2_nix and (is_psql_oriole or is_psql_17) - -- name: copy PG systemd unit - template: - src: files/postgresql_config/postgresql.service.j2 - dest: /etc/systemd/system/postgresql.service - when: debpkg_mode or stage2_nix - - name: copy optimizations systemd unit template: src: files/database-optimizations.service.j2 dest: /etc/systemd/system/database-optimizations.service when: debpkg_mode or stage2_nix - -- name: initialize pg required state - become: yes - shell: | - mkdir -p /run/postgresql - chown -R postgres:postgres /run/postgresql - when: stage2_nix and qemu_mode is defined - -- name: Restart Postgres Database without Systemd - become: yes - become_user: postgres - shell: | - source /var/lib/postgresql/.bashrc - /usr/lib/postgresql/bin/pg_ctl -D /var/lib/postgresql/data start - environment: - LANG: en_US.UTF-8 - LANGUAGE: en_US.UTF-8 - LC_ALL: en_US.UTF-8 - LC_CTYPE: en_US.UTF-8 - LOCALE_ARCHIVE: /usr/lib/locale/locale-archive - when: stage2_nix - - -# Reload -- name: System - systemd reload - systemd: - enabled: yes - name: postgresql - daemon_reload: yes - when: debpkg_mode or stage2_nix - - -- name: Add LOCALE_ARCHIVE to .bashrc - lineinfile: - dest: "/var/lib/postgresql/.bashrc" - line: 'export LOCALE_ARCHIVE=/usr/lib/locale/locale-archive' - create: yes - become: yes - when: nixpkg_mode - -- name: Add LANG items to .bashrc - lineinfile: - dest: "/var/lib/postgresql/.bashrc" - line: "{{ item }}" - loop: - - 'export LANG="en_US.UTF-8"' - - 'export LANGUAGE="en_US.UTF-8"' - - 'export LC_ALL="en_US.UTF-8"' - - 'export LANG="en_US.UTF-8"' - - 'export LC_CTYPE="en_US.UTF-8"' - become: yes - when: nixpkg_mode diff --git a/ansible/tests/conftest.py b/ansible/tests/conftest.py index 7e97be0cc..36df4dad9 100644 --- a/ansible/tests/conftest.py +++ b/ansible/tests/conftest.py @@ -59,13 +59,15 @@ def _run_playbook(playbook_name, verbose=False): ] if verbose: cmd.append("-vvv") - cmd.extend([ - "-i", - "localhost,", - "--extra-vars", - "@/flake/ansible/vars.yml", - f"/flake/ansible/tests/{playbook_name}", - ]) + cmd.extend( + [ + "-i", + "localhost,", + "--extra-vars", + "@/flake/ansible/vars.yml", + f"/flake/ansible/tests/{playbook_name}", + ] + ) result = host.run(" ".join(cmd)) if result.failed: console.log(result.stdout) @@ -73,4 +75,5 @@ def _run_playbook(playbook_name, verbose=False): raise pytest.fail( f"Ansible playbook {playbook_name} failed with return code {result.rc}" ) + return _run_playbook diff --git a/ansible/tests/test_nix.py b/ansible/tests/test_nix.py index fe900664d..ad85410b1 100644 --- a/ansible/tests/test_nix.py +++ b/ansible/tests/test_nix.py @@ -9,5 +9,6 @@ def run_ansible(run_ansible_playbook): def test_nix_service(host): assert host.service("nix-daemon.service").is_running + def test_envoy_service(host): assert host.service("envoy.service").is_running diff --git a/flake.lock b/flake.lock index ae6563b60..9b67032d5 100644 --- a/flake.lock +++ b/flake.lock @@ -248,15 +248,16 @@ ] }, "locked": { - "lastModified": 1754135474, - "narHash": "sha256-tZ8SXR80gcy8lqa1DD6/CNH9oQ1kOFw/cJihcn5/1M0=", + "lastModified": 1757363884, + "narHash": "sha256-lGG/LBllfniQvA6RrW2IRspX7p6lxM0NS8gKdQj7ilU=", "owner": "numtide", "repo": "system-manager", - "rev": "7865b4a207e46afa5c2e264de550730f8e281176", + "rev": "f35f51dc902264c2ccca010c313099bc53838601", "type": "github" }, "original": { "owner": "numtide", + "ref": "users", "repo": "system-manager", "type": "github" } diff --git a/flake.nix b/flake.nix index 813ffc69f..ecea0a91d 100644 --- a/flake.nix +++ b/flake.nix @@ -33,7 +33,8 @@ inputs.nixpkgs.follows = "nixpkgs"; }; system-manager = { - url = "github:numtide/system-manager"; + url = "github:numtide/system-manager/users"; + #url = "git+file:///home/jfroche/projects/numtide/system-manager/fix/return-tmpfile-error"; inputs.nixpkgs.follows = "nixpkgs"; }; }; diff --git a/nix/checks.nix b/nix/checks.nix index e2cf96c3e..1217a0312 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -13,7 +13,7 @@ psql_17 = self'.packages."psql_17/bin"; psql_orioledb-17 = self'.packages."psql_orioledb-17/bin"; pgroonga = self'.packages."psql_15/exts/pgroonga"; - inherit (self.supabase) defaults; + inherit (self.supabase.postgres) defaults; }; in { @@ -206,7 +206,7 @@ exit 1 } for i in {1..60}; do - if pg_isready -h ${self.supabase.defaults.host} -p ${pgPort}; then + if pg_isready -h ${self.supabase.postgres.defaults.host} -p ${pgPort}; then echo "PostgreSQL is ready" break fi @@ -220,8 +220,8 @@ exit 1 fi done - createdb -p ${pgPort} -h ${self.supabase.defaults.host} --username=supabase_admin testing - if ! psql -p ${pgPort} -h ${self.supabase.defaults.host} --username=supabase_admin -d testing -v ON_ERROR_STOP=1 -Xf ${./tests/prime.sql}; then + createdb -p ${pgPort} -h ${self.supabase.postgres.defaults.host} --username=supabase_admin testing + if ! psql -p ${pgPort} -h ${self.supabase.postgres.defaults.host} --username=supabase_admin -d testing -v ON_ERROR_STOP=1 -Xf ${./tests/prime.sql}; then echo "Error executing SQL file. PostgreSQL log content:" cat "$PGTAP_CLUSTER"/postgresql.log pg_ctl -D "$PGTAP_CLUSTER" stop @@ -229,7 +229,7 @@ fi SORTED_DIR=$(mktemp -d) for t in $(printf "%s\n" ${builtins.concatStringsSep " " sortedTestList}); do - psql -p ${pgPort} -h ${self.supabase.defaults.host} --username=supabase_admin -d testing -f "${./tests/sql}/$t.sql" || true + psql -p ${pgPort} -h ${self.supabase.postgres.defaults.host} --username=supabase_admin -d testing -f "${./tests/sql}/$t.sql" || true done rm -rf "$SORTED_DIR" pg_ctl -D "$PGTAP_CLUSTER" stop @@ -244,7 +244,7 @@ ${start-postgres-server-bin}/bin/start-postgres-server ${getVersionArg pgpkg} --daemonize for i in {1..60}; do - if pg_isready -h ${self.supabase.defaults.host} -p ${pgPort} -U supabase_admin -q; then + if pg_isready -h ${self.supabase.postgres.defaults.host} -p ${pgPort} -U supabase_admin -q; then echo "PostgreSQL is ready" break fi @@ -255,7 +255,7 @@ fi done - if ! psql -p ${pgPort} -h ${self.supabase.defaults.host} --no-password --username=supabase_admin -d postgres -v ON_ERROR_STOP=1 -Xf ${./tests/prime.sql}; then + if ! psql -p ${pgPort} -h ${self.supabase.postgres.defaults.host} --no-password --username=supabase_admin -d postgres -v ON_ERROR_STOP=1 -Xf ${./tests/prime.sql}; then echo "Error executing SQL file" exit 1 fi @@ -266,7 +266,7 @@ --dbname=postgres \ --inputdir=${./tests} \ --outputdir=$out/regression_output \ - --host=${self.supabase.defaults.host} \ + --host=${self.supabase.postgres.defaults.host} \ --port=${pgPort} \ --user=supabase_admin \ ${builtins.concatStringsSep " " sortedTestList}; then @@ -276,7 +276,7 @@ fi echo "Running migrations tests" - pg_prove -p ${pgPort} -U supabase_admin -h ${self.supabase.defaults.host} -d postgres -v ${../migrations/tests}/test.sql + pg_prove -p ${pgPort} -U supabase_admin -h ${self.supabase.postgres.defaults.host} -d postgres -v ${../migrations/tests}/test.sql # Copy logs to output for logfile in $(find /tmp -name postgresql.log -type f); do diff --git a/nix/config.nix b/nix/config.nix index 4076f0cea..0ef36f52d 100644 --- a/nix/config.nix +++ b/nix/config.nix @@ -14,6 +14,74 @@ let type = lib.types.str; default = "supabase_admin"; }; + settings = lib.mkOption { + type = lib.types.attrs; + default = { + authentication_timeout = "1min"; + "auto_explain.log_min_duration" = "10s"; + checkpoint_completion_target = "0.5"; + checkpoint_flush_after = "256kB"; + cluster_name = "main"; + "cron.database_name" = "postgres"; + default_text_search_config = "pg_catalog.english"; + effective_cache_size = "128MB"; + extra_float_digits = "0"; + ident_file = "/etc/postgresql/pg_ident.conf"; + jit = "off"; + jit_provider = "llvmjit"; + lc_messages = "en_US.UTF-8"; + lc_monetary = "en_US.UTF-8"; + lc_numeric = "en_US.UTF-8"; + lc_time = "en_US.UTF-8"; + listen_addresses = "*"; + log_destination = "stderr"; + log_line_prefix = "%h %m [%p] %q%u@%d "; + log_statement = "ddl"; + log_timezone = "UTC"; + max_replication_slots = "5"; + max_slot_wal_keep_size = "4096"; + max_wal_senders = "10"; + password_encryption = "scram-sha-256"; + port = 5432; + row_security = "on"; + shared_buffers = "128MB"; + ssl = "off"; + ssl_ca_file = ""; + ssl_cert_file = ""; + ssl_ciphers = "HIGH:MEDIUM:+3DES:!aNULL"; + ssl_crl_dir = ""; + ssl_crl_file = ""; + ssl_dh_params_file = ""; + ssl_ecdh_curve = "prime256v1"; + ssl_key_file = ""; + ssl_max_protocol_version = ""; + ssl_min_protocol_version = "TLSv1.2"; + ssl_passphrase_command = ""; + ssl_passphrase_command_supports_reload = "off"; + ssl_prefer_server_ciphers = "on"; + timezone = "UTC"; + wal_level = "logical"; + }; + }; + authentication = lib.mkOption { + type = lib.types.lines; + default = '' + # trust local connections + local all supabase_admin scram-sha-256 + local all all peer map=supabase_map + host all all 127.0.0.1/32 trust + host all all ::1/128 trust + + # IPv4 external connections + host all all 10.0.0.0/8 scram-sha-256 + host all all 172.16.0.0/12 scram-sha-256 + host all all 192.168.0.0/16 scram-sha-256 + host all all 0.0.0.0/0 scram-sha-256 + + # IPv6 external connections + host all all ::0/0 scram-sha-256 + ''; + }; }; }; postgresqlVersion = lib.types.submodule { @@ -24,7 +92,7 @@ let }; supabaseSubmodule = lib.types.submodule { options = { - defaults = lib.mkOption { type = postgresqlDefaults; }; + postgres.defaults = lib.mkOption { type = postgresqlDefaults; }; supportedPostgresVersions = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf postgresqlVersion); default = { }; @@ -38,7 +106,7 @@ in supabase = lib.mkOption { type = supabaseSubmodule; }; }; config.supabase = { - defaults = { }; + postgres.defaults = { }; supportedPostgresVersions = { postgres = { "15" = { diff --git a/nix/packages/default.nix b/nix/packages/default.nix index bdb5cd112..3acf98663 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -24,7 +24,7 @@ psql_17 = self'.packages."psql_17/bin"; psql_orioledb-17 = self'.packages."psql_orioledb-17/bin"; pgroonga = self'.packages."psql_15/exts/pgroonga"; - inherit (self.supabase) defaults; + inherit (self.supabase.postgres) defaults; }; in { @@ -35,7 +35,7 @@ inherit (self'.packages) docker-image-ubuntu; }; cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { }; - dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; }; + dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase.postgres) defaults; }; docker-image-ubuntu = pkgs.callPackage ./docker-ubuntu.nix { }; docs = pkgs.callPackage ./docs.nix { }; supabase-groonga = pkgs.callPackage ./groonga { }; @@ -46,13 +46,14 @@ pg-restore = pkgs.callPackage ./pg-restore.nix { psql_15 = self'.packages."psql_15/bin"; }; pg_prove = pkgs.perlPackages.TAPParserSourceHandlerpgTAP; pg_regress = makePgRegress activeVersion; + pgsodium_getkey_readonly = pkgs.callPackage ./pgsodium_getkey_readonly.nix { }; run-testinfra = pkgs.callPackage ./run-testinfra.nix { }; show-commands = pkgs.callPackage ./show-commands.nix { }; start-client = pkgs.callPackage ./start-client.nix { psql_15 = self'.packages."psql_15/bin"; psql_17 = self'.packages."psql_17/bin"; psql_orioledb-17 = self'.packages."psql_orioledb-17/bin"; - inherit (self.supabase) defaults; + inherit (self.supabase.postgres) defaults; }; start-replica = pkgs.callPackage ./start-replica.nix { psql_15 = self'.packages."psql_15/bin"; diff --git a/nix/packages/lib.nix b/nix/packages/lib.nix index fd3e29d76..d96765b61 100644 --- a/nix/packages/lib.nix +++ b/nix/packages/lib.nix @@ -58,7 +58,7 @@ }; postgresqlExtensionCustomScriptsPath = builtins.path { name = "extension-custom-scripts"; - path = ../../ansible/files/postgresql_extension_custom_scripts; + path = ../systemModules/postgres/extension-custom-scripts; }; getkeyScript = builtins.path { name = "pgsodium_getkey.sh"; diff --git a/nix/packages/pgsodium_getkey_readonly.nix b/nix/packages/pgsodium_getkey_readonly.nix new file mode 100644 index 000000000..2efd855da --- /dev/null +++ b/nix/packages/pgsodium_getkey_readonly.nix @@ -0,0 +1,20 @@ +{ + coreutils, + writeShellApplication, +}: +writeShellApplication { + name = "pgsodium-getkey-readonly"; + runtimeInputs = [ coreutils ]; + text = '' + KEY_FILE=/etc/postgresql-custom/pgsodium_root.key + + # On the hosted platform, the root key is generated and managed for each project + # If for some reason the key is missing, we want to fail loudly, + # rather than generating a new one. + if [[ ! -f "''${KEY_FILE}" ]]; then + echo "Key file ''${KEY_FILE} does not exist." >&2 + exit 1 + fi + cat "$KEY_FILE" + ''; +} diff --git a/nix/systemConfigs.nix b/nix/systemConfigs.nix index 7f50ded93..356e591f0 100644 --- a/nix/systemConfigs.nix +++ b/nix/systemConfigs.nix @@ -1,10 +1,21 @@ { self, inputs, ... }: let mkModules = system: [ - ({ - services.nginx.enable = true; - nixpkgs.hostPlatform = system; - }) + self.systemModules.postgres + ( + { pkgs, ... }: + { + services.nginx.enable = true; + nixpkgs.hostPlatform = system; + supabase.services.postgres = { + enable = true; + package = self.packages.${system}."psql_17/bin"; + initialScript = pkgs.writeText "init-script.sql" '' + CREATE USER supabase_auth_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD 'secret'; + ''; + }; + } + ) ]; systems = [ diff --git a/nix/systemModules/default.nix b/nix/systemModules/default.nix index 14b459255..4dd9eb6cd 100644 --- a/nix/systemModules/default.nix +++ b/nix/systemModules/default.nix @@ -4,6 +4,8 @@ { imports = [ ./tests ]; flake = { - systemModules = { }; + systemModules = { + postgres = ./postgres; + }; }; } diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix new file mode 100644 index 000000000..0ccda4830 --- /dev/null +++ b/nix/systemModules/postgres/default.nix @@ -0,0 +1,455 @@ +{ + lib, + config, + pkgs, + self, + ... +}: +let + cfg = config.supabase.services.postgres; + defaultUser = "postgres"; + defaultGroup = "postgres"; + + isOrioleDB = (builtins.match "[0-9][0-9]_.*" cfg.package.version) != null; + is17 = (builtins.substring 0 2 cfg.package.version) == "17"; + + toStr = + value: + if true == value then + "yes" + else if false == value then + "no" + else if builtins.isString value then + "'${lib.replaceStrings [ "'" ] [ "''" ] value}'" + else + builtins.toString value; + + # The main PostgreSQL configuration file. + configFile = pkgs.writeText "postgresql.conf" ( + lib.concatStringsSep "\n" ( + lib.mapAttrsToList (n: v: "${n} = ${toStr v}") ( + lib.filterAttrs (n: v: n != "includes" && v != null) cfg.settings + ) + ) + + "\n" + + lib.concatStringsSep "\n" (lib.map (f: "include '${f}'") cfg.settings.includes) + ); + pg_hba = pkgs.writeText "pg_hba.conf" ( + cfg.authentication + self.supabase.postgres.defaults.authentication + ); + pg_ident = pkgs.writeText "pg_ident.conf" '' + # MAPNAME SYSTEM-USERNAME PG-USERNAME + supabase_map postgres postgres + supabase_map root postgres + supabase_map ubuntu postgres + + # supabase-specific users + supabase_map gotrue supabase_auth_admin + supabase_map postgrest authenticator + supabase_map adminapi postgres + ''; + + read-replica-conf = pkgs.writeText "read-replica.conf" '' + # hot_standby = on + # restore_command = '/usr/bin/admin-mgr wal-fetch %f %p >> /var/log/wal-g/wal-fetch.log 2>&1' + # recovery_target_timeline = 'latest' + + # primary_conninfo = 'host=localhost port=6543 user=replication' + ''; + + supautils-conf = ./supautils.conf; +in +{ + options = { + supabase.services.postgres = { + enable = lib.mkEnableOption "Postgres"; + package = lib.mkOption { + type = lib.types.package; + description = '' + The package being used by postgresql. + ''; + }; + settings = lib.mkOption { + type = + with lib.types; + submodule { + freeformType = attrsOf (oneOf [ + bool + float + int + str + ]); + options = { + shared_preload_libraries = lib.mkOption { + type = nullOr (coercedTo (listOf str) (lib.concatStringsSep ",") commas); + default = null; + example = literalExpression ''[ "auto_explain" "anon" ]''; + description = '' + List of libraries to be preloaded. + ''; + }; + + includes = lib.mkOption { + type = listOf str; + default = [ + "/etc/postgresql-custom/read-replica.conf" + "/etc/postgresql-custom/supautils.conf" + ]; + description = '' + List of additional postgresql.conf files to be included. + ''; + }; + }; + }; + default = { }; + description = '' + PostgreSQL configuration. Refer to + + for an overview of `postgresql.conf`. + + ::: {.note} + 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. + ::: + ''; + }; + + authentication = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Defines how users authenticate themselves to the server. See the + [PostgreSQL documentation for pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) + for details on the expected format of this option. By default, + peer based authentication will be used for users connecting + via the Unix socket, and md5 password authentication will be + used for users connecting via TCP. Any added rules will be + inserted above the default rules. If you'd like to replace the + default rules entirely, you can use `lib.mkForce` in your + module. + ''; + }; + + environmentVariables = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { + LANG = "en_US.UTF-8"; + LANGUAGE = "en_US.UTF-8"; + LC_ALL = "en_US.UTF-8"; + LC_CTYPE = "en_US.UTF-8"; + LOCALE_ARCHIVE = "/usr/lib/locale/locale-archive"; + }; + description = '' + A set of environment variables to be exported in the global + environment for all users. These will be set in `/etc/profile.d/postgresql.sh`. + ''; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/data/pgdata"; + description = '' + The data directory for PostgreSQL. If left as the default value + this directory will automatically be created before the PostgreSQL server starts, otherwise + the sysadmin is responsible for ensuring the directory exists with appropriate ownership + and permissions. + ''; + }; + + superUser = lib.mkOption { + type = lib.types.str; + default = "supabase_admin"; + internal = true; + readOnly = true; + description = '' + PostgreSQL superuser account to use for various operations. Internal since changing + this value would lead to breakage while setting up databases. + ''; + }; + + initdbArgs = lib.mkOption { + type = with lib.types; listOf str; + default = [ + "--allow-group-access" + "--username=${cfg.superUser}" + ] + ++ (lib.optionals (isOrioleDB || is17) [ + "--locale-provider=icu" + "--encoding=UTF-8" + "--icu-locale=en_US.UTF-8" + ]); + description = '' + Additional arguments passed to `initdb` during data dir + initialisation. + ''; + }; + + initialScript = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = lib.literalExpression '' + pkgs.writeText "init-sql-script" ''' + alter user postgres with password 'myPassword'; + ''';''; + + description = '' + A file containing SQL statements to execute on first startup. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + + services.userborn.enable = true; + + users.groups.postgres = { }; + users.groups.ssl-cert = { }; + users.users.postgres = { + isSystemUser = true; + uid = config.ids.uids.postgres; + group = "postgres"; + home = "${cfg.dataDir}"; + shell = "/usr/bin/bash"; + }; + + systemd.tmpfiles.rules = [ + "d /home/postgres 0755 ${defaultUser} ${defaultGroup} -" + "d /var/log/postgresql 0755 ${defaultUser} ${defaultGroup} -" + "d /var/lib/postgresql 0755 ${defaultUser} ${defaultGroup} -" + "d /etc/postgresql 0775 ${defaultUser} ${defaultGroup} -" + "d /etc/postgresql-custom 0775 ${defaultUser} ${defaultGroup} -" + "d ${cfg.dataDir} 0750 ${defaultUser} ${defaultGroup} -" + "d /usr/lib/postgresql 0755 root root -" + + # Create symlinks + "L+ /var/lib/postgresql/data - - - - ${cfg.dataDir}" + "L+ /usr/lib/postgresql/bin - - - - ${cfg.package}/bin" + "L+ /usr/lib/postgresql/share - - - - ${cfg.package}/share" + "L+ /usr/lib/postgresql/lib - - - - ${cfg.package}/lib" + "L+ /etc/postgresql-custom/extension-custom-scripts - - - - ${./extension-custom-scripts}" + + # Copy configuration files + "C /etc/postgresql/pg_hba.conf 0440 ${defaultUser} ${defaultGroup} - ${pg_hba}" + "C /etc/postgresql/pg_ident.conf 0440 ${defaultUser} ${defaultGroup} - ${pg_ident}" + "C /etc/postgresql/postgresql.conf 0440 ${defaultUser} ${defaultGroup} - ${configFile}" + "C /etc/postgresql-custom/read-replica.conf 0664 ${defaultUser} ${defaultGroup} - ${read-replica-conf}" + "C /etc/postgresql-custom/supautils.conf 0664 ${defaultUser} ${defaultGroup} - ${supautils-conf}" + ]; + + environment = { + systemPackages = [ cfg.package ]; + + etc = { + "locale.gen".text = '' + C.UTF-8 UTF-8 + en_US.UTF-8 UTF-8 + ''; + "profile.d/postgresql.sh".text = builtins.concatStringsSep "\n" ( + lib.mapAttrsToList (key: value: ''export ${key}="${value}"'') (cfg.environmentVariables) + ); + }; + }; + + supabase.services.postgres.settings = lib.mkMerge [ + { + hba_file = "/etc/postgresql/pg_hba.conf"; + log_destination = "stderr"; + "pgsodium.getkey_script" = lib.getExe self.packages.${pkgs.system}.pgsodium_getkey_readonly; + "vault.getkey_script" = lib.getExe self.packages.${pkgs.system}.pgsodium_getkey_readonly; + shared_preload_libraries = [ + "auto_explain" + "pg_cron" + "pg_net" + "pg_stat_statements" + "pg_tle" + "pgaudit" + "pgsodium" + "plan_filter" + "plpgsql" + "plpgsql_check" + "supabase_vault" + "supautils" + ]; + } + (lib.mkIf ((lib.toInt (lib.versions.major cfg.package.version)) < 16) { + db_user_namespace = "off"; + shared_preload_libraries = [ "timescaledb" ]; + }) + self.supabase.postgres.defaults.settings + ]; + + systemd.targets.postgresql = { + description = "PostgreSQL"; + wantedBy = [ "system-manager.target" ]; + requires = [ + "postgresql.service" + "postgresql-setup.service" + ]; + }; + + systemd.services = { + postgresql = { + description = "PostgreSQL Server"; + + after = [ "network.target" ]; + + # To trigger the .target also on "systemctl start postgresql" as well as on + # restarts & stops. + # Please note that postgresql.service & postgresql.target binding to + # each other makes the Restart=always rule racy and results + # in sometimes the service not being restarted. + wants = [ "postgresql.target" ]; + partOf = [ "postgresql.target" ]; + wantedBy = [ "system-manager.target" ]; + + environment = { + PGDATA = cfg.dataDir; + } + // cfg.environmentVariables; + + path = [ cfg.package ]; + + preStart = '' + if ! test -e ${cfg.dataDir}/PG_VERSION; then + # Initialise the database. + initdb ${lib.escapeShellArgs cfg.initdbArgs} + fi + + # See postgresql-setup + touch "${cfg.dataDir}/.first_startup" + + if [ ! -f /etc/postgresql-custom/pgsodium_root.key ]; then + umask 077 + echo "0000000000000000000000000000000000000000000000000000000000000000" > /etc/postgresql-custom/pgsodium_root.key + fi + + # TODO postgres_prestart.sh logic here + ''; + + serviceConfig = { + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + User = "postgres"; + Group = "postgres"; + RuntimeDirectory = "postgresql"; + Type = "notify"; + + # Shut down Postgres using SIGINT ("Fast Shutdown mode"). See + # https://www.postgresql.org/docs/current/server-shutdown.html + KillSignal = "SIGINT"; + KillMode = "mixed"; + + # Give Postgres a decent amount of time to clean up after + # receiving systemd's SIGINT. + TimeoutSec = 120; + + ExecStart = "${cfg.package}/bin/postgres -c config_file=/etc/postgresql/postgresql.conf"; + + Restart = "always"; + + # Hardening + CapabilityBoundingSet = [ "" ]; + DevicePolicy = "closed"; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + MemoryDenyWriteExecute = true; # might be a problem for plv8 ? + NoNewPrivileges = true; + LockPersonality = true; + PrivateDevices = true; + PrivateMounts = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" # used for network interface enumeration + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + "@pkey" + ]; + UMask = "0077"; + ReadWritePaths = [ + cfg.dataDir + "/etc/postgresql-custom" + ]; + ReadOnlyPaths = [ + "/etc/postgresql" + ]; + }; + }; + postgresql-setup = { + description = "PostgreSQL Setup Scripts"; + + requires = [ "postgresql.service" ]; + after = [ "postgresql.service" ]; + + serviceConfig = { + User = "postgres"; + Group = "postgres"; + Type = "oneshot"; + RemainAfterExit = true; + }; + + path = [ cfg.package ]; + environment.PGPORT = builtins.toString cfg.settings.port; + + # Wait for PostgreSQL to be ready to accept connections. + script = '' + check-connection() { + psql -U ${cfg.superUser} -h localhost -d postgres -v ON_ERROR_STOP=1 <<-' EOF' + SELECT pg_is_in_recovery() \gset + \if :pg_is_in_recovery + \i still-recovering + \endif + EOF + } + while ! check-connection 2> /dev/null; do + if ! systemctl is-active --quiet postgresql.service; then exit 1; fi + sleep 0.1 + done + + if test -e "${cfg.dataDir}/.first_startup"; then + ${lib.optionalString (cfg.initialScript != null) '' + psql -U ${cfg.superUser} -h localhost -f "${cfg.initialScript}" -d postgres + ''} + rm -f "${cfg.dataDir}/.first_startup" + fi + ''; + }; + "setup-locales" = { + description = "Setup locales on the system"; + + before = [ "sysinit-reactivation.target" ]; + wantedBy = [ "sysinit-reactivation.target" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = lib.getExe ( + pkgs.writeShellScriptBin "setup-locales" '' + PATH=/usr/sbin:/usr/bin + /usr/sbin/locale-gen + /usr/sbin/update-locale + '' + ); + }; + }; + }; + }; +} diff --git a/ansible/files/postgresql_extension_custom_scripts/before-create.sql b/nix/systemModules/postgres/extension-custom-scripts/before-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/before-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/before-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/dblink/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/dblink/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/dblink/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/dblink/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pg_cron/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pg_cron/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pg_cron/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pg_cron/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pg_repack/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pg_repack/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pg_repack/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pg_repack/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pg_tle/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pg_tle/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pg_tle/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pg_tle/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pgmq/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pgmq/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pgmq/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pgmq/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pgsodium/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pgsodium/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pgsodium/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pgsodium/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pgsodium/before-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pgsodium/before-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pgsodium/before-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pgsodium/before-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/postgis_tiger_geocoder/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/postgis_tiger_geocoder/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/postgis_tiger_geocoder/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/postgis_tiger_geocoder/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/postgres_fdw/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/postgres_fdw/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/postgres_fdw/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/postgres_fdw/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/supabase_vault/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/supabase_vault/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/supabase_vault/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/supabase_vault/after-create.sql diff --git a/nix/systemModules/postgres/supautils.conf b/nix/systemModules/postgres/supautils.conf new file mode 100644 index 000000000..a3456f699 --- /dev/null +++ b/nix/systemModules/postgres/supautils.conf @@ -0,0 +1,15 @@ +supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"}}' +supautils.policy_grants = '{"postgres":["auth.audit_log_entries","auth.identities","auth.mfa_factors","auth.refresh_tokens","auth.sessions","auth.users","realtime.messages","storage.buckets","storage.migrations","storage.objects","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' +supautils.drop_trigger_grants = '{"postgres":["auth.audit_log_entries","auth.identities","auth.mfa_factors","auth.refresh_tokens","auth.sessions","auth.users","realtime.messages","storage.buckets","storage.migrations","storage.objects","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' +# full list: address_standardizer, address_standardizer_data_us, adminpack, amcheck, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, file_fdw, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intagg, intarray, isn, lo, ltree, moddatetime, old_snapshot, orioledb, pageinspect, pg_buffercache, pg_cron, pg_freespacemap, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_surgery, pg_tle, pg_trgm, pg_visibility, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgmq, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers, xml2 +# omitted because may be unsafe: adminpack, amcheck, file_fdw, lo, old_snapshot, pageinspect, pg_freespacemap, pg_surgery, pg_visibility +# omitted because deprecated: intagg, xml2 +# omitted because doesn't require superuser: pgmq +# omitted because protected: plpgsql +supautils.privileged_extensions = 'address_standardizer, address_standardizer_data_us, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intarray, isn, ltree, moddatetime, orioledb, pg_buffercache, pg_cron, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_tle, pg_trgm, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers' +supautils.extension_custom_scripts_path = '/etc/postgresql-custom/extension-custom-scripts' +supautils.privileged_extensions_superuser = 'supabase_admin' +supautils.privileged_role = 'postgres' +supautils.privileged_role_allowed_configs = 'auto_explain.*, log_lock_waits, log_min_duration_statement, log_min_messages, log_replication_commands, log_statement, log_temp_files, pg_net.batch_size, pg_net.ttl, pg_stat_statements.*, pgaudit.log, pgaudit.log_catalog, pgaudit.log_client, pgaudit.log_level, pgaudit.log_relation, pgaudit.log_rows, pgaudit.log_statement, pgaudit.log_statement_once, pgaudit.role, pgrst.*, plan_filter.*, safeupdate.enabled, session_replication_role, track_io_timing, wal_compression' +supautils.reserved_memberships = 'pg_read_server_files, pg_write_server_files, pg_execute_server_program, supabase_admin, supabase_auth_admin, supabase_storage_admin, supabase_read_only_user, supabase_realtime_admin, supabase_replication_admin, supabase_etl_admin, dashboard_user, pgbouncer, authenticator' +supautils.reserved_roles = 'supabase_admin, supabase_auth_admin, supabase_storage_admin, supabase_read_only_user, supabase_realtime_admin, supabase_replication_admin, supabase_etl_admin, dashboard_user, pgbouncer, service_role*, authenticator*, authenticated*, anon*' diff --git a/nix/systemModules/tests/test_postgres.py b/nix/systemModules/tests/test_postgres.py new file mode 100644 index 000000000..385bf1f8c --- /dev/null +++ b/nix/systemModules/tests/test_postgres.py @@ -0,0 +1,210 @@ +def test_postgres_directories(host): + """Test that all required PostgreSQL directories are created with correct permissions""" + directories = [ + ("/home/postgres", "755"), + ("/var/log/postgresql", "755"), + ("/var/lib/postgresql", "755"), + ("/etc/postgresql", "775"), + ("/etc/postgresql-custom", "775"), + ("/data/pgdata", "750"), + ("/usr/lib/postgresql", "755"), + ] + + for directory, expected_mode in directories: + dir_file = host.file(directory) + assert dir_file.is_directory, f"Directory {directory} should exist" + assert oct(dir_file.mode)[-3:] == expected_mode, ( + f"Directory {directory} should have mode {expected_mode}" + ) + + +def test_postgres_symlinks(host): + """Test that PostgreSQL symlinks are created correctly""" + symlinks = [ + "/var/lib/postgresql/data", + "/usr/lib/postgresql/bin", + ] + + for symlink in symlinks: + link_file = host.file(symlink) + assert link_file.is_symlink, f"File {symlink} should be a symlink" + + +def test_postgres_configuration_files(host): + """Test that PostgreSQL configuration files exist and have correct permissions""" + config_files = [ + ("/etc/postgresql/pg_hba.conf", "440"), + ("/etc/postgresql/postgresql.conf", "440"), + ] + + for config_file, expected_mode in config_files: + file_obj = host.file(config_file) + assert file_obj.is_file, f"Configuration file {config_file} should exist" + if expected_mode: + assert oct(file_obj.mode)[-3:] == expected_mode, ( + f"File {config_file} should have mode {expected_mode}" + ) + + +def test_postgres_package_installed(host): + """Test that PostgreSQL package is installed and available""" + # Check if postgres binaries are available + postgres_binaries = [ + "postgres", + "psql", + "createdb", + "dropdb", + "pg_dump", + "pg_restore", + ] + + for binary in postgres_binaries: + result = host.run(f"bash --login -c 'which {binary}'") + assert result.rc == 0, f"PostgreSQL binary {binary} should be available in PATH" + + +def test_postgres_environment_profile(host): + """Test that PostgreSQL environment variables are set in profile""" + profile_file = host.file("/etc/profile.d/postgresql.sh") + assert profile_file.is_file, "PostgreSQL profile script should exist" + + expected_vars = ["LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE"] + + for var in expected_vars: + result = host.run(f"bash --login -c 'echo ${var}'") + assert result.stdout.strip() != "", f"Environment variable {var} should be set" + + +def test_postgres_systemd_target(host): + """Test that PostgreSQL systemd target is configured""" + result = host.run("systemctl cat postgresql.target") + assert result.rc == 0, "PostgreSQL systemd target should exist" + + target_content = result.stdout + assert "Requires=postgresql.service postgresql-setup.service" in target_content + + system_manager_link = host.file( + "/etc/systemd/system/system-manager.target.wants/postgresql.target" + ) + assert system_manager_link.is_symlink, "System manager should have a symlink" + assert "postgresql.target" in system_manager_link.linked_to, ( + "System manager should have a symlink to postgresql.target" + ) + + +def test_postgres_data_directory_symlink(host): + """Test that the data directory symlink points to the correct location""" + data_link = host.file("/var/lib/postgresql/data") + assert data_link.is_symlink, "Data directory should be a symlink" + assert data_link.linked_to == "/data/pgdata", ( + "Data directory should link to /data/pgdata" + ) + + +def test_usr_lib_postgresql(host): + """Test that the bin directory symlink exists""" + for path in ["bin", "share", "lib"]: + dir_path = f"/usr/lib/postgresql/{path}" + dir_file = host.file(dir_path) + assert dir_file.is_symlink, f"Symlink {dir_path} should exist" + + +def test_postgres_configuration_content(host): + """Test that PostgreSQL configuration contains expected settings""" + config_file = host.file("/etc/postgresql/postgresql.conf") + assert config_file.is_file, "PostgreSQL configuration file should exist" + + content = config_file.content_string + assert "hba_file = '/etc/postgresql/pg_hba.conf'" in content + assert "log_destination = 'stderr'" in content + + +def test_postgres_hba_configuration(host): + """Test that pg_hba.conf file exists and is readable""" + hba_file = host.file("/etc/postgresql/pg_hba.conf") + assert hba_file.is_file, "pg_hba.conf file should exist" + assert hba_file.user == "postgres", "pg_hba.conf should be owned by root" + assert hba_file.group == "postgres", "pg_hba.conf should be owned by root group" + + +def test_postgres_directory_ownership(host): + """Test that PostgreSQL directories have correct ownership""" + directories = [ + "/home/postgres", + "/var/log/postgresql", + "/var/lib/postgresql", + "/etc/postgresql", + "/etc/postgresql-custom", + "/data/pgdata", + ] + + for directory in directories: + dir_file = host.file(directory) + assert dir_file.user == "postgres", ( + f"Directory {directory} should be owned by root" + ) + assert dir_file.group == "postgres", ( + f"Directory {directory} should be owned by root group" + ) + + +def test_postgres_configuration(host): + """Test that PostgreSQL configuration file exists and has correct permissions""" + config_file = host.file("/etc/postgresql/postgresql.conf") + assert config_file.is_file, "PostgreSQL configuration file should exist" + assert oct(config_file.mode)[-3:] == "440", ( + "PostgreSQL configuration file should have mode 440" + ) + assert config_file.user == "postgres", ( + "PostgreSQL configuration file should be owned by postgres" + ) + assert config_file.group == "postgres", ( + "PostgreSQL configuration file should be owned by postgres group" + ) + + # db_user_namespace doesn't exist in postgres >= 16 + assert not config_file.contains("db_user_namespace") + + +def test_locales(host): + installed_locales = host.run("localectl list-locales").stdout + assert "C.UTF-8" in installed_locales, "C.UTF-8 locale should be installed" + assert "en_US.UTF-8" in installed_locales, "en_US.UTF-8 locale should be installed" + + +def test_postgres_service_running(host): + assert host.service("postgresql.service").is_valid + assert host.service("postgresql.service").is_running, ( + "postgresql service should be running but failed: {}".format( + host.run( + "systemctl status postgresql.service; journalctl -n 100 -u postgresql.service" + ).stdout + ) + ) + + required_logs = [ + 'Using language tag "en-US" for ICU locale "en_US.UTF-8"', + "locale provider: icu", + "default collation: en-US", + "LC_COLLATE: en_US.UTF-8", + "LC_CTYPE: en_US.UTF-8", + "LC_MESSAGES: en_US.UTF-8", + "LC_MONETARY: en_US.UTF-8", + "LC_NUMERIC: en_US.UTF-8", + "vault primary server secret key loaded", + "pg_cron scheduler started", + ] + logs = host.run("journalctl -u postgresql.service --no-pager").stdout + for log in required_logs: + assert log in logs, f"Log '{log}' should be present in PostgreSQL logs" + + +def test_postgres_setup_service_running(host): + assert host.service("postgresql-setup.service").is_valid + assert host.service("postgresql-setup.service").is_running, ( + "postgresql-setup service should be running but failed: {}".format( + host.run( + "systemctl status postgresql-setup.service; journalctl -n 100 -u postgresql-setup.service" + ).stdout + ) + )