diff --git a/.github/actions/nix-install-ephemeral/action.yml b/.github/actions/nix-install-ephemeral/action.yml index 5dbdabe8e..c999971e4 100644 --- a/.github/actions/nix-install-ephemeral/action.yml +++ b/.github/actions/nix-install-ephemeral/action.yml @@ -48,4 +48,6 @@ runs: substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ${{ inputs.push-to-cache == 'true' && 'post-build-hook = /etc/nix/upload-to-cache.sh' || '' }} + extra-experimental-features = auto-allocate-uids cgroups + auto-allocate-uids = true max-jobs = 4 diff --git a/ansible/playbook.yml b/ansible/playbook.yml index fbc3d5d81..2e9a7d8ba 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -146,7 +146,11 @@ tags: - install-supabase-internal when: debpkg_mode or nixpkg_mode - + + - name: deploy system-manager + import_tasks: tasks/setup-system-manager.yml + when: debpkg_mode or stage2_nix + - name: Enhance fail2ban import_tasks: tasks/setup-fail2ban.yml when: debpkg_mode or nixpkg_mode diff --git a/ansible/tasks/setup-nix.yml b/ansible/tasks/setup-nix.yml new file mode 100644 index 000000000..9675677dd --- /dev/null +++ b/ansible/tasks/setup-nix.yml @@ -0,0 +1,11 @@ +--- +- name: Check if nix is installed + ansible.builtin.command: which nix + register: nix_installed + failed_when: nix_installed.rc != 0 + ignore_errors: true + +- name: Install nix + ansible.builtin.shell: curl --proto '=https' --tlsv1.2 -sSf -L https://artifacts.nixos.org/experimental-installer | sh -s -- install --no-confirm --extra-conf 'substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com' --extra-conf 'trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI=% cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=' + when: nix_installed.rc != 0 + become: true diff --git a/ansible/tasks/setup-system-manager.yml b/ansible/tasks/setup-system-manager.yml new file mode 100644 index 000000000..cd76bbb4d --- /dev/null +++ b/ansible/tasks/setup-system-manager.yml @@ -0,0 +1,7 @@ +--- +- name: Deploy system manager + ansible.builtin.shell: | + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh + cd /tmp + nix run --accept-flake-config /tmp/flake#system-manager -- switch --flake /tmp/flake 2>&1 | tee /tmp/system-manager-deploy.log + become: true diff --git a/ansible/vars.yml b/ansible/vars.yml index ae5ecfb33..39e6b2084 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -10,9 +10,9 @@ postgres_major: # Full version strings for each major version postgres_release: - postgresorioledb-17: "17.6.0.053-orioledb" - postgres17: "17.6.1.096" - postgres15: "15.14.1.096" + postgresorioledb-17: "17.6.0.053-orioledb-sysmg-1" + postgres17: "17.6.1.096-sysmg-1" + postgres15: "15.14.1.096-sysmg-1" # Non Postgres Extensions pgbouncer_release: 1.25.1 diff --git a/audit-specs/baselines/ami-build/user.yml b/audit-specs/baselines/ami-build/user.yml index e764be626..a3b1e716c 100644 --- a/audit-specs/baselines/ami-build/user.yml +++ b/audit-specs/baselines/ami-build/user.yml @@ -6,14 +6,14 @@ user: root: exists: true home: /root - shell: /bin/bash + shell: /run/system-manager/sw/bin/bash ubuntu: exists: true home: /home/ubuntu shell: /bin/bash nobody: exists: true - shell: /usr/sbin/nologin + shell: /run/system-manager/sw/bin/nologin # PostgreSQL ecosystem postgres: diff --git a/flake.lock b/flake.lock index 763447438..612833035 100644 --- a/flake.lock +++ b/flake.lock @@ -36,6 +36,22 @@ "type": "github" } }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": "nixpkgs-lib" @@ -54,6 +70,28 @@ "type": "github" } }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "system-manager", + "userborn", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768135262, + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -115,6 +153,29 @@ "type": "github" } }, + "gitignore_2": { + "inputs": { + "nixpkgs": [ + "system-manager", + "userborn", + "pre-commit-hooks-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "multigres": { "flake": false, "locked": { @@ -292,6 +353,34 @@ "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" } }, + "pre-commit-hooks-nix": { + "inputs": { + "flake-compat": [ + "system-manager", + "userborn", + "flake-compat" + ], + "gitignore": "gitignore_2", + "nixpkgs": [ + "system-manager", + "userborn", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769069492, + "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, "root": { "inputs": { "devshell": "devshell", @@ -306,6 +395,7 @@ "nixpkgs": "nixpkgs_2", "nixpkgs-oldstable": "nixpkgs-oldstable", "rust-overlay": "rust-overlay", + "system-manager": "system-manager", "treefmt-nix": "treefmt-nix" } }, @@ -329,6 +419,27 @@ "type": "github" } }, + "system-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "userborn": "userborn" + }, + "locked": { + "lastModified": 1772184297, + "narHash": "sha256-hDt6ez1H5Cpkciose8cDclJWHbBPEMmJNphSAxKvkUc=", + "owner": "numtide", + "repo": "system-manager", + "rev": "6f5d1b4458db2b2776f76bb6449100bd0fabc1d8", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "system-manager", + "type": "github" + } + }, "systems": { "locked": { "lastModified": 1681028828, @@ -344,6 +455,21 @@ "type": "github" } }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ @@ -363,6 +489,32 @@ "repo": "treefmt-nix", "type": "github" } + }, + "userborn": { + "inputs": { + "flake-compat": "flake-compat_2", + "flake-parts": "flake-parts_2", + "nixpkgs": [ + "system-manager", + "nixpkgs" + ], + "pre-commit-hooks-nix": "pre-commit-hooks-nix", + "systems": "systems_2" + }, + "locked": { + "lastModified": 1770377964, + "narHash": "sha256-q2pnlX2IW0kg80GLFnwWd/GigIpkuZnyKPLhrgJql3E=", + "owner": "jfroche", + "repo": "userborn", + "rev": "55c2cd7952c207a62736a5bbd9499ea73da18d24", + "type": "github" + }, + "original": { + "owner": "jfroche", + "ref": "system-manager", + "repo": "userborn", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index dbabf994c..7f2ebe31d 100644 --- a/flake.nix +++ b/flake.nix @@ -33,6 +33,8 @@ rust-overlay.url = "github:oxalica/rust-overlay"; treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; treefmt-nix.url = "github:numtide/treefmt-nix"; + system-manager.inputs.nixpkgs.follows = "nixpkgs"; + system-manager.url = "github:numtide/system-manager"; }; outputs = @@ -55,6 +57,8 @@ nix/nixpkgs.nix nix/packages nix/overlays + nix/systemModules + nix/systemConfigs.nix ]; }); } diff --git a/nix/docs/README.md b/nix/docs/README.md index 5d177b13d..9b2f8c03c 100644 --- a/nix/docs/README.md +++ b/nix/docs/README.md @@ -19,6 +19,7 @@ learn how to play with `postgres` in the [build guide](./build-postgres.md). - **[Start Client/Server](./start-client-server.md)** - Running PostgreSQL client and server - **[Docker](./docker.md)** - Docker integration and usage - **[Docker Image Size Analyzer](./image-size-analyzer-usage.md)** - Tool to analyze the Docker image sizes +- **[System Manager](./system-manager.md)** - Declarative system configuration with system-manager - **[Use direnv](./use-direnv.md)** - Development environment with direnv - **[Pre-commit Hooks](./pre-commit-hooks.md)** - Automatic formatting and code checks before commits - **[Nix Formatter](./nix-formatter.md)** - Code formatting with treefmt diff --git a/nix/docs/nix-directory-structure.md b/nix/docs/nix-directory-structure.md index c58441b4b..33b8feaac 100644 --- a/nix/docs/nix-directory-structure.md +++ b/nix/docs/nix-directory-structure.md @@ -22,7 +22,9 @@ nix/ ├── ext/ # PostgreSQL extensions ├── overlays/ # Nixpkgs overlays ├── packages/ # Custom packages - └── postgresql/ # PostgreSQL packages + ├── postgresql/ # PostgreSQL packages + ├── systemConfigs.nix # system-manager configuration definitions + └── systemModules/ # system-manager service modules ``` ## Module Descriptions @@ -150,6 +152,20 @@ Nixpkgs overlays for package customization: - `cargo-pgrx-0-11-3.nix` - PGRX toolchain overlay - `psql_16-oriole.nix` - OrioleDB PostgreSQL variant +#### `nix/systemConfigs.nix` + +System configuration definitions for [system-manager](https://github.com/numtide/system-manager). +Calls `system-manager.lib.makeSystemConfig` to produce a configuration for each supported architecture (`aarch64-linux`, `x86_64-linux`) from the enabled modules. +See [System manager](./system-manager.md) for details. + +#### `nix/systemModules/` + +Service module definitions managed by system-manager: + +- `default.nix` - Module registry that exports modules under `flake.systemModules` +- Individual `.nix` files - Service modules (e.g. nginx) loaded via `flake-parts-lib.importApply` +- `tests/default.nix` - Container-based tests using `makeContainerTest` + #### `nix/cargo-pgrx/` Rust-based PostgreSQL extension building: diff --git a/nix/docs/system-manager.md b/nix/docs/system-manager.md new file mode 100644 index 000000000..0b8f8386d --- /dev/null +++ b/nix/docs/system-manager.md @@ -0,0 +1,234 @@ +# System manager + +[system-manager](https://github.com/numtide/system-manager) provides declarative, Nix-based system configuration management for non-NixOS Linux systems. +It replaces imperative service setup with reproducible Nix module definitions, bringing NixOS-style service management to the AMI build without requiring a full NixOS installation. + +## How it fits into the AMI build pipeline + +The AMI build uses a two-stage pipeline orchestrated by Packer and Ansible. +Stage 1 installs Nix itself, while stage 2 uses Nix to build and deploy all services. +system-manager is deployed during stage 2 via the Ansible task `ansible/tasks/setup-system-manager.yml`: + +```yaml +- name: Deploy system manager + ansible.builtin.shell: | + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh + cd /tmp + nix run --accept-flake-config /tmp/flake#system-manager -- switch --flake /tmp/flake 2>&1 | tee /tmp/system-manager-deploy.log + become: true +``` + +This sources the Nix daemon profile, then runs `system-manager switch` against the flake to apply the declared system configuration. + +## Nix configuration walkthrough + +### Flake input + +The system-manager flake input is declared in `flake.nix` (lines 34-35), pinned to the upstream repository with nixpkgs following the main input: + +```nix +system-manager.inputs.nixpkgs.follows = "nixpkgs"; +system-manager.url = "github:numtide/system-manager"; +``` + +The flake outputs import both the module registry and the system configurations: + +```nix +imports = [ + # ... + nix/systemModules + nix/systemConfigs.nix +]; +``` + +### System configurations + +`nix/systemConfigs.nix` defines the top-level system configurations for each supported architecture. +It calls `system-manager.lib.makeSystemConfig` to produce a configuration from the enabled modules: + +```nix +mkSystemConfig = system: { + name = system; + value.default = inputs.system-manager.lib.makeSystemConfig { + modules = mkModules system; + extraSpecialArgs = { + inherit self; + inherit system; + }; + }; +}; +``` + +The `mkModules` function returns the list of modules to enable. +Currently it enables the nginx service and sets the host platform: + +```nix +mkModules = system: [ + ({ + services.nginx.enable = true; + nixpkgs.hostPlatform = system; + }) +]; +``` + +Configurations are built for both `aarch64-linux` and `x86_64-linux`. + +### System modules + +`nix/systemModules/default.nix` is the module registry. +It is a flake-parts module that exports individual system modules under `flake.systemModules`: + +```nix +{ + imports = [ ./tests ]; + flake = { + systemModules = { + nginx = flake-parts-lib.importApply ./nginx.nix { inherit withSystem self; }; + }; + }; +} +``` + +Each module is loaded with `flake-parts-lib.importApply`, which passes `withSystem` and `self` as arguments to the module file. + +## Adding a new system module + +To add a new system module: + +1. Create a new `.nix` file under `nix/systemModules/`, for example `nix/systemModules/my-service.nix`. + The module is a standard NixOS-style module with options and config: + + ```nix + { + lib, + config, + ... + }: + let + cfg = config.supabase.services.my-service; + in + { + options = { + supabase.services.my-service = { + enable = lib.mkEnableOption "Whether to enable the my-service systemd service."; + }; + }; + + config = lib.mkIf cfg.enable { + # systemd units, environment.etc entries, etc. + }; + } + ``` + +2. Register the module in `nix/systemModules/default.nix` by adding it to the `systemModules` attribute set: + + ```nix + systemModules = { + my-service = ./my-service.nix; + }; + ``` + +3. Include and enable the module in `nix/systemConfigs.nix` by adding it to the `mkModules` list and setting the enable option: + + ```nix + mkModules = system: [ + self.systemModules.my-service + ({ + services.nginx.enable = true; + supabase.services.my-service.enable = true; + nixpkgs.hostPlatform = system; + }) + ]; + ``` + +4. Add a test assertion to the test script in `nix/systemModules/tests/default.nix` (see below). + +## Testing + +### Container tests + +Tests are defined in `nix/systemModules/tests/default.nix` using `system-manager.lib.containerTest.makeContainerTest`. +This creates a lightweight container-based NixOS test that validates the system configuration: + +```nix +check-system-manager = + let + toplevel = self.systemConfigs.${pkgs.system}.default; + in + inputs.system-manager.lib.containerTest.makeContainerTest { + hostPkgs = pkgs; + name = "check-system-manager"; + inherit toplevel; + testScript = '' + start_all() + + machine.wait_for_unit("multi-user.target") + + machine.activate() + machine.wait_for_unit("system-manager.target") + + with subtest("Verify nginx service"): + assert machine.service("nginx").is_running, "nginx should be running" + ''; + }; +``` + +The test script starts the container, waits for systemd to reach `multi-user.target`, activates the system-manager configuration, then verifies that managed services are running. +When adding a new module, extend the `testScript` with an additional `subtest` block that asserts the new service is running. + +### Running tests locally + +The container tests use `systemd-nspawn` which requires the `uid-range` nix feature. This in turn requires `auto-allocate-uids` and the `auto-allocate-uids` experimental feature to be enabled on the Linux machine running the tests. + +**On macOS:** These tests cannot run natively on macOS. You need to enter the shell of a Linux VM (e.g. an Ubuntu VM via OrbStack, UTM, or similar) and run the tests from there. + +Ensure your Linux machine's `/etc/nix/nix.conf` includes: + +``` +auto-allocate-uids = true +extra-experimental-features = nix-command flakes auto-allocate-uids cgroups +trusted-users = root @wheel @sudo +``` + +After updating the config, restart the nix daemon: + +```bash +sudo systemctl restart nix-daemon +``` + +Then run the system-manager check: + +```bash +nix build .#checks.aarch64-linux.check-system-manager -L +``` + +Or for x86_64: + +```bash +nix build .#checks.x86_64-linux.check-system-manager -L +``` + +The `-L` flag streams build logs for visibility. +These checks only run on Linux (gated by `lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux`). + +## CI integration + +The `check-system-manager` derivation is part of the flake's `checks` output, so it runs automatically in the `nix-build-checks-*` jobs of the main `nix-build.yml` workflow alongside all other checks. + +## Runtime effects + +After `system-manager switch` runs, managed software is available under `/run/system-manager/sw/`. +This affects paths throughout the system. +For example, the audit baseline `audit-specs/baselines/ami-build/user.yml` references these paths for user shells: + +```yaml +root: + exists: true + home: /root + shell: /run/system-manager/sw/bin/bash +nobody: + exists: true + shell: /run/system-manager/sw/bin/nologin +``` + +When adding new services or modifying system-manager configuration, update the audit baselines accordingly to reflect any changes to user shells, service users, or file paths that `supascan` validates during AMI builds. diff --git a/nix/mkdocs.yml b/nix/mkdocs.yml index 52c2be539..b4374ced1 100644 --- a/nix/mkdocs.yml +++ b/nix/mkdocs.yml @@ -26,6 +26,7 @@ nav: - Create pgrx Extension: creating-pgrx-extension.md - Update pgrx Extensions: updating-pgrx-extensions.md - Receipt Files: receipt-files.md + - System manager: system-manager.md - Package Management: - Adding New Packages: adding-new-package.md - Update Extensions: update-extension.md diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 7b1a6ea54..48f56d626 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -115,6 +115,9 @@ cargo-pgrx_0_14_3 ; } + // lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { + system-manager = inputs'.system-manager.packages.default; + } // lib.optionalAttrs pkgs.stdenv.isDarwin { setup-darwin-linux-builder = pkgs.callPackage ./setup-darwin-linux-builder.nix { inherit inputs self; diff --git a/nix/systemConfigs.nix b/nix/systemConfigs.nix new file mode 100644 index 000000000..2dda5edea --- /dev/null +++ b/nix/systemConfigs.nix @@ -0,0 +1,30 @@ +{ self, inputs, ... }: +let + mkModules = system: [ + self.systemModules.genesis + ({ + nixpkgs.hostPlatform = system; + }) + ]; + + systems = [ + "aarch64-linux" + "x86_64-linux" + ]; + + mkSystemConfig = system: { + name = system; + value.default = inputs.system-manager.lib.makeSystemConfig { + modules = mkModules system; + extraSpecialArgs = { + inherit self; + inherit system; + }; + }; + }; +in +{ + flake = { + systemConfigs = builtins.listToAttrs (map mkSystemConfig systems); + }; +} diff --git a/nix/systemModules/default.nix b/nix/systemModules/default.nix new file mode 100644 index 000000000..04161ef9f --- /dev/null +++ b/nix/systemModules/default.nix @@ -0,0 +1,20 @@ +{ + ... +}: +{ + imports = [ ./tests ]; + flake = { + systemModules = { + genesis = { + #this file is just a placeholder to bootstrap + #the system manager, it will be replaced by real configurations + environment.etc."system-manager-genesis" = { + text = ""; + user = "root"; + group = "root"; + mode = "0644"; + }; + }; + }; + }; +} diff --git a/nix/systemModules/tests/default.nix b/nix/systemModules/tests/default.nix new file mode 100644 index 000000000..a9178a963 --- /dev/null +++ b/nix/systemModules/tests/default.nix @@ -0,0 +1,36 @@ +{ self, inputs, ... }: +{ + perSystem = + { + lib, + pkgs, + ... + }: + { + checks = lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { + check-system-manager = + let + toplevel = self.systemConfigs.${pkgs.system}.default; + in + inputs.system-manager.lib.containerTest.makeContainerTest { + hostPkgs = pkgs; + name = "check-system-manager"; + inherit toplevel; + testScript = '' + start_all() + + machine.wait_for_unit("multi-user.target") + + machine.activate() + machine.wait_for_unit("system-manager.target") + + with subtest("Verify genesis file"): + assert machine.file("/etc/system-manager-genesis").exists, "/etc/system-manager-genesis should exist" + assert machine.file("/etc/system-manager-genesis").mode == 0o644, "/etc/system-manager-genesis should have mode 0644" + assert machine.file("/etc/system-manager-genesis").user == "root", "/etc/system-manager-genesis should be owned by root" + assert machine.file("/etc/system-manager-genesis").group == "root", "/etc/system-manager-genesis should be owned by root" + ''; + }; + }; + }; +} diff --git a/stage2-nix-psql.pkr.hcl b/stage2-nix-psql.pkr.hcl index 032dd71e5..d751a506d 100644 --- a/stage2-nix-psql.pkr.hcl +++ b/stage2-nix-psql.pkr.hcl @@ -120,6 +120,11 @@ build { destination = "/tmp/ansible-playbook" } + provisioner "file" { + source = "${abspath(path.root)}" + destination = "/tmp/flake" + } + provisioner "shell" { environment_vars = [ "GIT_SHA=${var.git_sha}",