From 1c1e415b4b0ef738d34a3a93b9d84dca50d2cb75 Mon Sep 17 00:00:00 2001 From: WilliButz <willibutz@posteo.de> Date: Mon, 2 Sep 2024 15:06:39 +0200 Subject: [PATCH] init docs --- .github/workflows/render-options.yml | 27 ++ .gitignore | 1 + README.md | 38 +-- docs/.gitignore | 1 + docs/book.toml | 10 + docs/custom.css | 4 + docs/flake.lock | 27 ++ docs/flake.nix | 60 ++++ docs/src/SUMMARY.md | 9 + docs/src/examples.md | 131 ++++++++ docs/src/impermanence-comparison.md | 35 ++ docs/src/impermanence-migration.md | 111 ++++++ docs/src/impermanence.md | 7 + docs/src/library-and-testing.md | 14 + flake.nix | 2 +- module.nix | 456 +------------------------ options.nix | 482 +++++++++++++++++++++++++++ 17 files changed, 930 insertions(+), 485 deletions(-) create mode 100644 .github/workflows/render-options.yml create mode 100644 docs/.gitignore create mode 100644 docs/book.toml create mode 100644 docs/custom.css create mode 100644 docs/flake.lock create mode 100644 docs/flake.nix create mode 100644 docs/src/SUMMARY.md create mode 100644 docs/src/examples.md create mode 100644 docs/src/impermanence-comparison.md create mode 100644 docs/src/impermanence-migration.md create mode 100644 docs/src/impermanence.md create mode 100644 docs/src/library-and-testing.md create mode 100644 options.nix diff --git a/.github/workflows/render-options.yml b/.github/workflows/render-options.yml new file mode 100644 index 0000000..db61f85 --- /dev/null +++ b/.github/workflows/render-options.yml @@ -0,0 +1,27 @@ +name: Docs +on: + push: + branches: + - main +jobs: + render: + name: Render and Deploy + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@ab6bcb2d5af0e904d04aea750e2089e9dc4cbfdd + with: + diagnostic-endpoint: "" + + - uses: DeterminateSystems/magic-nix-cache-action@b46e247b898aa56e6d2d2e728dc6df6c84fdb738 + with: + diagnostic-endpoint: "" + + - run: nix build -L ./docs#packages.x86_64-linux.docs + + - uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./result diff --git a/.gitignore b/.gitignore index 96dec6d..95162d1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ result* /.direnv /tests/.nixos-test-history +*.html diff --git a/README.md b/README.md index 9f5867a..2ec734d 100644 --- a/README.md +++ b/README.md @@ -5,47 +5,23 @@ Nix tooling to enable declarative management of non-volatile system state. Inspired and heavily influenced by [impermanence](https://github.com/nix-community/impermanence) but not meant to be a drop-in replacement. -## Work in Progress +## Documentation -🚧 still under construction 🚧 +Docs are available at <https://willibutz.github.io/preservation> -Check out [the test](tests/basic.nix) for a usage example 👀 +## Prerequisites -Depends on https://github.com/NixOS/nixpkgs/pull/307528 (merged) - -## How does it compare to impermanence - -* Preservation does not attempt to be a very generic solution, it tries to fill a specific niche. - Specifically Preservation does not support non-NixOS systems via home-manager, which is supported - by impermanence. - -* Preservation only creates static configuration for - [systemd-tmpfiles](https://www.freedesktop.org/software/systemd/man/latest/systemd-tmpfiles.html) - and systemd [mount units](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html). - This makes Preservation a potential candidate for state management on interpreter-less systems. - - Impermanence makes use of NixOS activation scripts and custom systemd services with bash (at the point - of writing this), to create and configure files and directories. - -* Preservation must be precisely configured, there is no [special runtime logic](https://github.com/nix-community/impermanence/blob/23c1f06316b67cb5dabdfe2973da3785cfe9c34a/mount-file.bash#L31-L42) - in place. This means that the user must define: - * when the preservation should be set up: either in the initrd, or after (the default) - * how the preservation should be set up: either by symlink, or bindmount (the default) - * whether or not parent directories of the persisted files require special permissions - -* Preservation's configuration is based on, and very similar to that of impermanence. - -* Preservation uses a global `enable` option, impermanence does not (see https://github.com/nix-community/impermanence/pull/171) +Depends on <https://github.com/NixOS/nixpkgs/pull/307528> (merged, available on nixos-unstable). ## Why? This aims to provide a declarative state management solution for NixOS systems without resorting to interpreters to do the heavy lifting. This should enable impermanence-like state management on -an "interpreter-less" system. +an "interpreter-less" NixOS system. Related: -- https://github.com/NixOS/nixpkgs/issues/265640 -- https://github.com/nix-community/projects/blob/main/proposals/nixpkgs-security-phase2.md#boot-chain-security +- <https://github.com/NixOS/nixpkgs/issues/265640> +- <https://github.com/nix-community/projects/blob/main/proposals/nixpkgs-security-phase2.md#boot-chain-security> ## License diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..323666f --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,10 @@ +[book] +title = "Preservation" +authors = ["Willi Butz"] +language = "en" +src = "src" + +[output.html] +additional-css = ["custom.css"] +git-repository-url = "https://github.com/willibutz/preservation" +edit-url-template = "https://github.com/willibutz/preservation/edit/main/docs/{path}" diff --git a/docs/custom.css b/docs/custom.css new file mode 100644 index 0000000..738eee0 --- /dev/null +++ b/docs/custom.css @@ -0,0 +1,4 @@ +:root { + /* accommodate long option paths on big screens */ + --content-max-width: 1000px; +} diff --git a/docs/flake.lock b/docs/flake.lock new file mode 100644 index 0000000..27fefb3 --- /dev/null +++ b/docs/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1724819573, + "narHash": "sha256-GnR7/ibgIH1vhoy8cYdmXE6iyZqKqFxQSVkFgosBh6w=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "71e91c409d1e654808b2621f28a327acfdad8dc2", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/docs/flake.nix b/docs/flake.nix new file mode 100644 index 0000000..0f9bd0e --- /dev/null +++ b/docs/flake.nix @@ -0,0 +1,60 @@ +{ + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = inputs: { + packages.x86_64-linux = + let + pkgs = import inputs.nixpkgs { system = "x86_64-linux"; }; + in + { + optionsManualMD = + let + eval = pkgs.lib.evalModules { + modules = [ + ../options.nix + (args: { + options._module.args = args.lib.mkOption { internal = true; }; + }) + ]; + }; + optionsDoc = pkgs.nixosOptionsDoc { + inherit (eval) options; + transformOptions = + o: + o + // { + declarations = map ( + declaration: + let + flakeOutPath = inputs.self.sourceInfo.outPath; + name = pkgs.lib.removePrefix "${flakeOutPath}/" declaration; + in + if pkgs.lib.hasPrefix "${flakeOutPath}/" declaration then + { + inherit name; + url = "https://github.com/willibutz/preservation/blob/main/${name}"; + } + else + declaration + ) o.declarations; + }; + }; + in + optionsDoc.optionsCommonMark; + + docs = pkgs.stdenv.mkDerivation { + name = "preservation-docs"; + src = pkgs.lib.cleanSource ../.; + nativeBuildInputs = [ pkgs.mdbook ]; + patchPhase = '' + cat ${inputs.self.packages.x86_64-linux.optionsManualMD} > docs/src/configuration-options.md + ''; + buildPhase = '' + cd docs + mdbook build + ''; + installPhase = "cp -vr book $out"; + }; + }; + }; +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 0000000..d5ca1c8 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,9 @@ +# Summary + +[Preservation](../../README.md) +- [Configuration Options](./configuration-options.md) +- [Examples](./examples.md) +- [Impermanence](./impermanence.md) + - [Comparison](./impermanence-comparison.md) + - [Migration](./impermanence-migration.md) +- [Library and Testing](./library-and-testing.md) diff --git a/docs/src/examples.md b/docs/src/examples.md new file mode 100644 index 0000000..1159725 --- /dev/null +++ b/docs/src/examples.md @@ -0,0 +1,131 @@ +# Examples + +See [Configuration Options](./configuration-options.md) for all available options. + +## Simple + +```nix +# configuration.nix +{ + config, + lib, + pkgs, + ... +}: +{ + preservation = { + enable = true; + preserveAt."/persistent" = { + files = [ + # auto-generated machine ID + { file = "/etc/machine-id"; inInitrd = true; } + ]; + directories = [ + "/var/lib/systemd/timers" + # NixOS user state + "/var/lib/nixos" + # preparing /var/log early (inInitrd) avoids a dependency cycle (see TODO.md) + { directory = "/var/log"; inInitrd = true; } + ]; + }; + }; +} +``` + +## Complex + +```nix +# configuration.nix +{ + config, + lib, + pkgs, + ... +}: +{ + preservation = { + # the module doesn't do anything unless it is enabled + enable = true; + + preserveAt."/persistent" = { + + # preserve system directories + directories = [ + "/etc/secureboot" + "/var/lib/bluetooth" + "/var/lib/fprint" + "/var/lib/fwupd" + "/var/lib/libvirt" + "/var/lib/power-profiles-daemon" + "/var/lib/systemd/coredump" + "/var/lib/systemd/rfkill" + "/var/lib/systemd/timers" + { directory = "/var/lib/nixos"; inInitrd = true; } + + # preparing /var/log early (inInitrd) avoids a dependency cycle (see TODO.md) + { directory = "/var/log"; inInitrd = true; } + ]; + + # preserve system files + files = [ + { file = "/etc/machine-id"; inInitrd = true; } + "/etc/ssh/ssh_host_ed25519_key" + "/etc/ssh/ssh_host_rsa_key" + "/var/lib/usbguard/rules.conf" + + # creates a symlink on the volatile root + # creates an empty directory on the persistent volume, i.e. /persistent/var/lib/systemd + # does not create an empty file at the symlink's target (would require `createLinkTarget = true`) + { file = "/var/lib/systemd/random-seed"; how = "symlink"; inInitrd = true; configureParent = true; } + ]; + + # preserve user-specific files, implies ownership + users = { + butz = { + directories = [ + { directory = ".ssh"; mode = "0700"; } + ".config/syncthing" + ".config/Element" + ".local/state/nvim" + ".local/state/wireplumber" + ".local/share/direnv" + ".local/state/nix" + ".mozilla" + ]; + files = [ + ".histfile" + ]; + }; + users.root = { + # specify user home when it is not `/home/${user}` + home = "/root"; + directories = [ + { directory = ".ssh"; mode = "0700"; } + ]; + }; + }; + }; + }; + + # Create some directories with custom permissions. + # + # In this configuration the path `/home/butz/.local` is not an immediate parent + # of any persisted file, so it would be created with the systemd-tmpfiles default + # ownership `root:root` and mode `0755`. This would mean that the user `butz` + # could not create other files or directories inside `/home/butz/.local`. + # + # Therefore systemd-tmpfiles is used to prepare such directories with + # appropriate permissions. + # + # Note that immediate parent directories of persisted files can also be + # configured with ownership and permissions from the `parent` settings if + # `configureParent = true` is set for the file. + systemd.tmpfiles.settings.preservation = { + "/home/butz/.config".d = { user = "butz"; group = "users"; mode = "0755"; }; + "/home/butz/.local".d = { user = "butz"; group = "users"; mode = "0755"; }; + "/home/butz/.local/share".d = { user = "butz"; group = "users"; mode = "0755"; }; + "/home/butz/.local/state".d = { user = "butz"; group = "users"; mode = "0755"; }; + }; + +} +``` diff --git a/docs/src/impermanence-comparison.md b/docs/src/impermanence-comparison.md new file mode 100644 index 0000000..18bb09f --- /dev/null +++ b/docs/src/impermanence-comparison.md @@ -0,0 +1,35 @@ +# How does Preservation compare to [impermanence](https://github.com/nix-community/impermanence) + +### Preservation does not attempt to be a very generic solution + +Preservation tries to fill a specific niche. +For instance, Preservation does not support non-NixOS systems via home-manager, which is supported +by impermanence. See [Migration](./impermanence-migration.md) for more technical details. + +### Preservation only generates static configuration + +That is configuration for [systemd-tmpfiles](https://www.freedesktop.org/software/systemd/man/latest/systemd-tmpfiles.html) +and systemd [mount units](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html). +This makes Preservation a potential candidate for state management on interpreter-less NixOS systems. + +Impermanence makes use of NixOS activation scripts and custom systemd services with bash (at the point +of writing this), to create files and directories, setup mounts and configure ownership and permissions (see next point). + +### Preservation must be precisely configured +There is no [special runtime logic](https://github.com/nix-community/impermanence/blob/23c1f06316b67cb5dabdfe2973da3785cfe9c34a/mount-file.bash#L31-L42) + in place. This means that the user must define: + * when the preservation should be set up: either in the initrd, or after (the default) + * how the preservation should be set up: either by symlink, or bindmount (the default) + * whether or not parent directories of the persisted files require special permissions + +See [Migration](./impermanence-migration.md) for specifics that need to be considered when coming from an impermanence setup. + +### Similar configuration + +Preservation's configuration is based on, and very similar to that of impermanence. See [Migration](./impermanence-migration.md) for technical details. + +### Global `enable` option + +Preservation uses a global `enable` option, impermanence does not. + +For thoughts on the `enable` option, see the discussion at <https://github.com/nix-community/impermanence/pull/171> and for available configuration options see [Configuration Options](./configuration-options.md). diff --git a/docs/src/impermanence-migration.md b/docs/src/impermanence-migration.md new file mode 100644 index 0000000..d1d2b71 --- /dev/null +++ b/docs/src/impermanence-migration.md @@ -0,0 +1,111 @@ +# Migration from impermanence to Preservation + +This section lists individual differences between impermanence and +Preservation, to better understand them in context of a complete configuration +[Examples](./examples.md) may be helpful. + +The following points need to be considered when migrating an existing +impermanence configuration to Preservation: + +### Global `enable` switch + +The module must be explicitly enabled by setting `preservation.enable` to `true`. + +### When to persist + +Files and directories that need to be persisted early, must be explicitly configured. For example `/etc/machine-id`: + +This file needs to be persisted very early, by explicitly setting `inInitrd` to `true`: +```nix +preservation.preserveAt."/persistent".files = [ + { file = "/etc/machine-id"; inInitrd = true; } +]; +``` + +### How to persist + +The mode of preservation must be set explicitly for some files and directories. +This can be done by setting `how` to either `symlink` or `bindmount` (default). +For most cases the default is sufficient but sometimes a symlink may be needed, +for example `/var/lib/systemd/random-seed`. + +This file is expected to not exist before it is initialized. A symlink can be +used to cause its creation to happen on the persistent volume: + +```nix +preservation.preserveAt."/persistent".files = [ + { + file = "/var/lib/systemd/random-seed"; + # create a symlink on the volatile volume + how = "symlink"; + # prepare the preservation early during startup + inInitrd = true; + } +]; +``` + +Note that no file is created at the symlink's target, unless `createLinkTarget` is set to `true`. + +### Configuration of intermediate path components + +Preservation does not handle any files or directories other than those specifically configured +to be preserved, and optionally their immediate parent directories (via `configureParent` and +the `parent` options). + +All missing components of a preserved path that do not already exist, are created by +systemd-tmpfiles with default ownership `root:root` and mode `0755`. + +Should such directories require different ownership or mode, the intended way to provision them +is directly via systemd-tmpfiles. + +**Example** + +Consider a preserved file `/foo/bar/baz`: + +```nix +preservation.preserveAt."/persistent".files = [ + { file = "/foo/bar/baz"; user = "baz"; group = "baz"; }; +]; +``` + +This would create the file with desired ownership on both the volatile and persistent volumes. +However, the parent directories that did not exist before, i.e. `/foo` and `/foo/bar`, are +created with ownership `root:root` and mode `0755`. + +Preservations allows the configuration of immediate parents, so the permissions for `/foo/bar` +can be configured: +```nix +preservation.preserveAt."/persistent".files = [ + { + file = "/foo/bar/baz"; user = "baz"; group = "baz"; + configureParent = true; + parent.user = "baz"; + parent.group = "bar"; + }; +]; +``` +Now the parent directory `/foo/bar` is configured with ownership `baz:bar`. But the first +path component `/foo` still has systemd-tmpfiles' default ownership and the configuration +becomes quite convoluted. + +**Solution** + +To create or configure intermediate path components of a persisted path, systemd-tmpfiles +may be used directly: + +```nix +# configure preservation of single file +preservation.preserveAt."/persistent".files = [ + { file = "/foo/bar/baz"; user = "baz"; group = "bar"; }; +]; + +# create and configure parents of preserved file on the volatile volume with custom permissions +# The Preservation module also uses `settings.preservation` here. +systemd.tmpfiles.settings.preservation = { + "/foo".d = { user = "foo"; group = "bar"; mode = "0775"; }; + "/foo/bar".d = { user = "bar"; group = "bar"; mode = "0755"; }; +}; +``` + +See [tmpfiles.d(5)](https://www.freedesktop.org/software/systemd/man/latest/tmpfiles.d.html) +for available configuration options. diff --git a/docs/src/impermanence.md b/docs/src/impermanence.md new file mode 100644 index 0000000..da5d181 --- /dev/null +++ b/docs/src/impermanence.md @@ -0,0 +1,7 @@ +# Impermanence + +[Impermanence](https://github.com/nix-community/impermanence) is the established solution for managing persistent state on NixOS systems. +Preservation is inspired and heavily influenced by impermanence. + +See [Comparison](./impermanence-comparison.md) for a high-level overview of their differences. + diff --git a/docs/src/library-and-testing.md b/docs/src/library-and-testing.md new file mode 100644 index 0000000..5139c87 --- /dev/null +++ b/docs/src/library-and-testing.md @@ -0,0 +1,14 @@ +# Library and Testing + +## Library + +The functionality that is used in the module to discover the files and +directories that are persisted and to generate the corresponding tmpfiles +config and mount units is available from [`lib.nix`](../../lib.nix). +It is also available from the flake `lib` output. + +In both cases it needs to be instantiated with the nixpkgs `lib`. + +## Testing + +The integration test(s) can be found in [/tests](../../tests). diff --git a/flake.nix b/flake.nix index fc1b3e0..3655a26 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,6 @@ { outputs = inputs: { - lib = import ./lib.nix; # need to pass '{ lib, ... }' + lib = import ./lib.nix; # need to pass nixpkgs-lib nixosModules = { default = inputs.self.nixosModules.preservation; preservation = import ./module.nix; diff --git a/module.nix b/module.nix index 473e949..9daaa2a 100644 --- a/module.nix +++ b/module.nix @@ -9,461 +9,11 @@ let mkRegularTmpfilesRules mkInitrdTmpfilesRules ; - - mountOption = lib.types.submodule { - options = { - name = lib.mkOption { - type = lib.types.str; - description = '' - Specify the name of the mount option. - ''; - example = "bind"; - }; - value = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - Optionally specify a value for the mount option. - ''; - }; - }; - }; - - directoryPath = - attrs@{ defaultOwner, ... }: - { - options = { - directory = lib.mkOption { - type = lib.types.str; - description = '' - Specify the path to the directory that should be preserved. - ''; - }; - how = lib.mkOption { - type = lib.types.enum [ - "bindmount" - "symlink" - ]; - default = "bindmount"; - description = '' - Specify how this directory should be preserved. - ''; - }; - user = lib.mkOption { - type = lib.types.str; - default = defaultOwner; - description = '' - Specify the user that owns the directory. - ''; - }; - group = lib.mkOption { - type = lib.types.str; - default = config.users.users.${defaultOwner}.group; - description = '' - Specify the group that owns the directory. - ''; - }; - mode = lib.mkOption { - type = lib.types.str; - default = "0755"; - description = '' - Specify the access mode of the directory. - See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information. - ''; - }; - configureParent = lib.mkOption { - type = lib.types.bool; - default = attrs.config.how == "symlink" && attrs.config.user != "root"; - description = '' - Specify whether the parent directory of this directory shall be configured with - custom ownership and permissions. - - By default, missing parent directories are always created with ownership - `root:root` and mode `0755`, as described in {manpage}`tmpfiles.d(5)`. - - Ownership and mode may be configured through the options - {option}`parent.user`, - {option}`parent.group`, - {option}`parent.mode`. - - Defaults to `true` when {option}`how` is set to `symlink` and - {option}`user` is not `root`. - ''; - }; - parent.user = lib.mkOption { - type = lib.types.str; - default = defaultOwner; - description = '' - Specify the user that owns the parent directory of this file. - ''; - }; - parent.group = lib.mkOption { - type = lib.types.str; - default = config.users.users.${defaultOwner}.group; - description = '' - Specify the group that owns the parent directory of this file. - ''; - }; - parent.mode = lib.mkOption { - type = lib.types.str; - default = "0755"; - description = '' - Specify the access mode of the parent directory of this file. - See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information. - ''; - }; - mountOptions = lib.mkOption { - type = with lib.types; listOf (coercedTo str (n: { name = n; }) mountOption); - default = [ - "bind" - "X-fstrim.notrim" # see fstrim(8) - ]; - description = '' - Specify a list of mount options that should be used for this directory. - These options are only used when {option}`how` is set to `bindmount`. - ''; - }; - createLinkTarget = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Only used when {option}`how` is set to `symlink`. - - Specify whether to create an empty directory with the specified ownership - and permissions as target of the symlink. - ''; - }; - inInitrd = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Whether to prepare preservation of this directory in initrd. - - ::: {.note} - For most directories there is no need to enable this option. - ::: - - ::: {.important} - Note that both owner and group for this directory need to be - available in the initrd for permissions to be set correctly. - ::: - ''; - }; - }; - }; - - filePath = - attrs@{ defaultOwner, ... }: - { - options = { - file = lib.mkOption { - type = lib.types.str; - description = '' - Specify the path to the file that should be preserved. - ''; - }; - how = lib.mkOption { - type = lib.types.enum [ - "bindmount" - "symlink" - ]; - default = "bindmount"; - description = '' - Specify how this file should be preserved: - - 1. Either a file is placed both on the volatile and on the - persistent volume, with a bind mount from the former to the - latter. - - 2. Or a symlink is created on the volatile volume, pointing - to the corresponding location on the persistent volume. - ''; - }; - user = lib.mkOption { - type = lib.types.str; - default = defaultOwner; - description = '' - Specify the user that owns the file. - ''; - }; - group = lib.mkOption { - type = lib.types.str; - default = config.users.users.${defaultOwner}.group; - description = '' - Specify the group that owns the file. - ''; - }; - mode = lib.mkOption { - type = lib.types.str; - default = "0644"; - description = '' - Specify the access mode of the file. - See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information. - ''; - }; - configureParent = lib.mkOption { - type = lib.types.bool; - default = attrs.config.how == "symlink" && attrs.config.user != "root"; - description = '' - Specify whether the parent directory of this file shall be configured with - custom ownership and permissions. - - By default, missing parent directories are always created with ownership - `root:root` and mode `0755`, as described in {manpage}`tmpfiles.d(5)`. - - Ownership and mode may be configured through the options - {option}`parent.user`, - {option}`parent.group`, - {option}`parent.mode`. - - Defaults to `true` when {option}`how` is set to `symlink` and - {option}`user` is not `root`. - ''; - }; - parent.user = lib.mkOption { - type = lib.types.str; - default = defaultOwner; - description = '' - Specify the user that owns the parent directory of this file. - ''; - }; - parent.group = lib.mkOption { - type = lib.types.str; - default = config.users.users.${attrs.defaultOwner}.group; - description = '' - Specify the group that owns the parent directory of this file. - ''; - }; - parent.mode = lib.mkOption { - type = lib.types.str; - default = "0755"; - description = '' - Specify the access mode of the parent directory of this file. - See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information. - ''; - }; - mountOptions = lib.mkOption { - type = with lib.types; listOf (coercedTo str (o: { name = o; }) mountOption); - default = [ "bind" ]; - description = '' - Specify a list of mount options that should be used for this file. - These options are only used when {option}`how` is set to `bindmount`. - ''; - }; - createLinkTarget = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Only used when {option}`how` is set to `symlink`. - - Specify whether to create an empty file with the specified ownership - and permissions as target of the symlink. - ''; - }; - inInitrd = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Whether to prepare preservation of this file in the initrd. - - ::: {.note} - For most files there is no need to enable this option. - - {file}`/etc/machine-id` is an exception because it needs to - be populated/read very early. - ::: - - ::: {.important} - Note that both owner and group for this file need to be - available in the initrd for permissions to be set correctly. - ::: - ''; - }; - }; - }; - - userModule = - attrs@{ name, ... }: - { - options = { - username = lib.mkOption { - type = with lib.types; passwdEntry str; - default = name; - description = '' - Specify the user for which the {option}`directories` and {option}`files` - should be persisted. Defaults to the name of the parent attribute set. - ''; - }; - home = lib.mkOption { - type = with lib.types; passwdEntry path; - default = config.users.users.${name}.home; - description = '' - Specify the path to the user's home directory. - ''; - }; - directories = lib.mkOption { - type = - with lib.types; - listOf ( - coercedTo str (d: { directory = d; }) (submodule [ - { _module.args.defaultOwner = attrs.config.username; } - directoryPath - ]) - ); - default = [ ]; - apply = map (d: d // { directory = "${attrs.config.home}/${d.directory}"; }); - description = '' - Specify a list of directories that should be preserved for this user. - The paths are interpreted relative to {option}`home`. - ''; - example = [ ".rabbit_hole" ]; - }; - files = lib.mkOption { - type = - with lib.types; - listOf ( - coercedTo str (f: { file = f; }) (submodule [ - { _module.args.defaultOwner = attrs.config.username; } - filePath - ]) - ); - default = [ ]; - apply = map (f: f // { file = "${attrs.config.home}/${f.file}"; }); - description = '' - Specify a list of files that should be preserved for this user. - The paths are interpreted relative to {option}`home`. - ''; - example = [ - { - file = ".config/foo"; - mode = "0600"; - } - "bar" - ]; - }; - }; - }; - - preserveAtSubmodule = - attrs@{ name, ... }: - { - options = { - persistentStoragePath = lib.mkOption { - type = lib.types.path; - default = name; - description = '' - Specify the location at which the {option}`directories`, {option}`files`, - {option}`users.directories` and {option}`users.files` should be preserved. - Defaults to the name of the parent attribute set. - ''; - }; - directories = lib.mkOption { - type = - with lib.types; - listOf ( - coercedTo str (d: { directory = d; }) (submodule [ - { _module.args.defaultOwner = "root"; } - directoryPath - ]) - ); - default = [ ]; - description = '' - Specify a list of directories that should be preserved. - The paths are interpreted as absolute paths. - ''; - example = [ "/var/lib/someservice" ]; - }; - files = lib.mkOption { - type = - with lib.types; - listOf ( - coercedTo str (f: { file = f; }) (submodule [ - { _module.args.defaultOwner = "root"; } - filePath - ]) - ); - default = [ ]; - description = '' - Specify a list of files that should be preserved. - The paths are interpreted as absolute paths. - ''; - example = [ - { - file = "/etc/wpa_supplicant.conf"; - how = "symlink"; - } - { - file = "/etc/machine-id"; - inInitrd = true; - } - ]; - }; - users = lib.mkOption { - type = with lib.types; attrsOf (submodule userModule); - default = { }; - description = '' - Specify a set of users with corresponding files and directories that - should be preserved. - ''; - example = { - alice.directories = [ ".rabbit_hole" ]; - butz = { - files = [ - { - file = ".config/foo"; - mode = "0600"; - } - "bar" - ]; - directories = [ "unshaved_yaks" ]; - }; - }; - }; - }; - }; - in { - options.preservation = { - enable = lib.mkEnableOption "the preservation module"; - - preserveAt = lib.mkOption { - type = with lib.types; attrsOf (submodule preserveAtSubmodule); - description = '' - Specify a set of locations and the corresponding state that - should be preserved there. - ''; - default = { }; - example = { - "/state" = { - directories = [ "/var/lib/someservice" ]; - files = [ - { - file = "/etc/wpa_supplicant.conf"; - how = "symlink"; - } - { - file = "/etc/machine-id"; - inInitrd = true; - } - ]; - users = { - alice.directories = [ ".rabbit_hole" ]; - butz = { - files = [ - { - file = ".config/foo"; - mode = "0600"; - } - "bar" - ]; - directories = [ "unshaved_yaks" ]; - }; - }; - }; - }; - }; - }; + imports = [ + ./options.nix + ]; config = lib.mkIf cfg.enable { assertions = [ diff --git a/options.nix b/options.nix new file mode 100644 index 0000000..00e3c33 --- /dev/null +++ b/options.nix @@ -0,0 +1,482 @@ +{ config, lib, ... }: + +let + mountOption = lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = '' + Specify the name of the mount option. + ''; + example = "bind"; + }; + value = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + Optionally specify a value for the mount option. + ''; + }; + }; + }; + + directoryPath = + attrs@{ defaultOwner, ... }: + { + options = { + directory = lib.mkOption { + type = lib.types.str; + description = '' + Specify the path to the directory that should be preserved. + ''; + }; + how = lib.mkOption { + type = lib.types.enum [ + "bindmount" + "symlink" + ]; + default = "bindmount"; + description = '' + Specify how this directory should be preserved. + ''; + }; + user = lib.mkOption { + type = lib.types.str; + default = defaultOwner; + description = '' + Specify the user that owns the directory. + ''; + }; + group = lib.mkOption { + type = lib.types.str; + default = config.users.users.${defaultOwner}.group; + defaultText = "config.users.users.\${defaultOwner}.group"; + description = '' + Specify the group that owns the directory. + ''; + }; + mode = lib.mkOption { + type = lib.types.str; + default = "0755"; + description = '' + Specify the access mode of the directory. + See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information. + ''; + }; + configureParent = lib.mkOption { + type = lib.types.bool; + default = attrs.config.how == "symlink" && attrs.config.user != "root"; + description = '' + Specify whether the parent directory of this directory shall be configured with + custom ownership and permissions. + + By default, missing parent directories are always created with ownership + `root:root` and mode `0755`, as described in {manpage}`tmpfiles.d(5)`. + + Ownership and mode may be configured through the options + {option}`parent.user`, + {option}`parent.group`, + {option}`parent.mode`. + + Defaults to `true` when {option}`how` is set to `symlink` and + {option}`user` is not `root`. + ''; + }; + parent.user = lib.mkOption { + type = lib.types.str; + default = defaultOwner; + description = '' + Specify the user that owns the parent directory of this file. + ''; + }; + parent.group = lib.mkOption { + type = lib.types.str; + default = config.users.users.${defaultOwner}.group; + defaultText = "config.users.users.\${defaultOwner}.group"; + description = '' + Specify the group that owns the parent directory of this file. + ''; + }; + parent.mode = lib.mkOption { + type = lib.types.str; + default = "0755"; + description = '' + Specify the access mode of the parent directory of this file. + See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information. + ''; + }; + mountOptions = lib.mkOption { + type = with lib.types; listOf (coercedTo str (n: { name = n; }) mountOption); + default = [ + "bind" + "X-fstrim.notrim" # see fstrim(8) + ]; + description = '' + Specify a list of mount options that should be used for this directory. + These options are only used when {option}`how` is set to `bindmount`. + ''; + }; + createLinkTarget = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Only used when {option}`how` is set to `symlink`. + + Specify whether to create an empty directory with the specified ownership + and permissions as target of the symlink. + ''; + }; + inInitrd = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to prepare preservation of this directory in initrd. + + ::: {.note} + For most directories there is no need to enable this option. + ::: + + ::: {.important} + Note that both owner and group for this directory need to be + available in the initrd for permissions to be set correctly. + ::: + ''; + }; + }; + }; + + filePath = + attrs@{ defaultOwner, ... }: + { + options = { + file = lib.mkOption { + type = lib.types.str; + description = '' + Specify the path to the file that should be preserved. + ''; + }; + how = lib.mkOption { + type = lib.types.enum [ + "bindmount" + "symlink" + ]; + default = "bindmount"; + description = '' + Specify how this file should be preserved: + + 1. Either a file is placed both on the volatile and on the + persistent volume, with a bind mount from the former to the + latter. + + 2. Or a symlink is created on the volatile volume, pointing + to the corresponding location on the persistent volume. + ''; + }; + user = lib.mkOption { + type = lib.types.str; + default = defaultOwner; + description = '' + Specify the user that owns the file. + ''; + }; + group = lib.mkOption { + type = lib.types.str; + default = config.users.users.${defaultOwner}.group; + defaultText = "config.users.users.\${defaultOwner}.group"; + description = '' + Specify the group that owns the file. + ''; + }; + mode = lib.mkOption { + type = lib.types.str; + default = "0644"; + description = '' + Specify the access mode of the file. + See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information. + ''; + }; + configureParent = lib.mkOption { + type = lib.types.bool; + default = attrs.config.how == "symlink" && attrs.config.user != "root"; + description = '' + Specify whether the parent directory of this file shall be configured with + custom ownership and permissions. + + By default, missing parent directories are always created with ownership + `root:root` and mode `0755`, as described in {manpage}`tmpfiles.d(5)`. + + Ownership and mode may be configured through the options + {option}`parent.user`, + {option}`parent.group`, + {option}`parent.mode`. + + Defaults to `true` when {option}`how` is set to `symlink` and + {option}`user` is not `root`. + ''; + }; + parent.user = lib.mkOption { + type = lib.types.str; + default = defaultOwner; + description = '' + Specify the user that owns the parent directory of this file. + ''; + }; + parent.group = lib.mkOption { + type = lib.types.str; + default = config.users.users.${attrs.defaultOwner}.group; + defaultText = "config.users.users.\${defaultOwner}.group"; + description = '' + Specify the group that owns the parent directory of this file. + ''; + }; + parent.mode = lib.mkOption { + type = lib.types.str; + default = "0755"; + description = '' + Specify the access mode of the parent directory of this file. + See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information. + ''; + }; + mountOptions = lib.mkOption { + type = with lib.types; listOf (coercedTo str (o: { name = o; }) mountOption); + default = [ "bind" ]; + description = '' + Specify a list of mount options that should be used for this file. + These options are only used when {option}`how` is set to `bindmount`. + ''; + }; + createLinkTarget = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Only used when {option}`how` is set to `symlink`. + + Specify whether to create an empty file with the specified ownership + and permissions as target of the symlink. + ''; + }; + inInitrd = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to prepare preservation of this file in the initrd. + + ::: {.note} + For most files there is no need to enable this option. + + {file}`/etc/machine-id` is an exception because it needs to + be populated/read very early. + ::: + + ::: {.important} + Note that both owner and group for this file need to be + available in the initrd for permissions to be set correctly. + ::: + ''; + }; + }; + }; + + userModule = + attrs@{ name, ... }: + { + options = { + username = lib.mkOption { + type = with lib.types; passwdEntry str; + default = name; + description = '' + Specify the user for which the {option}`directories` and {option}`files` + should be persisted. Defaults to the name of the parent attribute set. + ''; + }; + home = lib.mkOption { + type = with lib.types; passwdEntry path; + default = config.users.users.${name}.home; + defaultText = "config.users.users.\${name}.home"; + description = '' + Specify the path to the user's home directory. + ''; + }; + directories = lib.mkOption { + type = + with lib.types; + listOf ( + coercedTo str (d: { directory = d; }) (submodule [ + { _module.args.defaultOwner = attrs.config.username; } + directoryPath + ]) + ); + default = [ ]; + apply = map (d: d // { directory = "${attrs.config.home}/${d.directory}"; }); + description = '' + Specify a list of directories that should be preserved for this user. + The paths are interpreted relative to {option}`home`. + ''; + example = [ ".rabbit_hole" ]; + }; + files = lib.mkOption { + type = + with lib.types; + listOf ( + coercedTo str (f: { file = f; }) (submodule [ + { _module.args.defaultOwner = attrs.config.username; } + filePath + ]) + ); + default = [ ]; + apply = map (f: f // { file = "${attrs.config.home}/${f.file}"; }); + description = '' + Specify a list of files that should be preserved for this user. + The paths are interpreted relative to {option}`home`. + ''; + example = lib.literalMD '' + ```nix + [ + { + file = ".config/foo"; + mode = "0600"; + } + "bar" + ] + ``` + ''; + }; + }; + config = { + _module.args.name = lib.mkOverride 1499 "‹user›"; + }; + }; + + preserveAtSubmodule = + attrs@{ name, ... }: + { + options = { + persistentStoragePath = lib.mkOption { + type = lib.types.path; + default = name; + description = '' + Specify the location at which the {option}`directories`, {option}`files`, + {option}`users.directories` and {option}`users.files` should be preserved. + Defaults to the name of the parent attribute set. + ''; + }; + directories = lib.mkOption { + type = + with lib.types; + listOf ( + coercedTo str (d: { directory = d; }) (submodule [ + { _module.args.defaultOwner = "root"; } + directoryPath + ]) + ); + default = [ ]; + description = '' + Specify a list of directories that should be preserved. + The paths are interpreted as absolute paths. + ''; + example = [ "/var/lib/someservice" ]; + }; + files = lib.mkOption { + type = + with lib.types; + listOf ( + coercedTo str (f: { file = f; }) (submodule [ + { _module.args.defaultOwner = "root"; } + filePath + ]) + ); + default = [ ]; + description = '' + Specify a list of files that should be preserved. + The paths are interpreted as absolute paths. + ''; + example = lib.literalMD '' + ```nix + [ + { + file = "/etc/wpa_supplicant.conf"; + how = "symlink"; + } + { + file = "/etc/machine-id"; + inInitrd = true; + } + ] + ``` + ''; + }; + users = lib.mkOption { + type = with lib.types; attrsOf (submodule userModule); + default = { }; + description = '' + Specify a set of users with corresponding files and directories that + should be preserved. + ''; + example = lib.literalMD '' + ```nix + { + alice.directories = [ ".rabbit_hole" ]; + butz = { + files = [ + { + file = ".config/foo"; + mode = "0600"; + } + "bar" + ]; + directories = [ "unshaved_yaks" ]; + }; + } + ``` + ''; + }; + }; + }; + +in +{ + options.preservation = { + enable = lib.mkEnableOption "the preservation module"; + + preserveAt = lib.mkOption { + type = with lib.types; attrsOf (submodule preserveAtSubmodule); + description = '' + Specify a set of locations and the corresponding state that + should be preserved there. + ''; + default = { }; + example = lib.literalMD '' + ```nix + { + "/state" = { + directories = [ "/var/lib/someservice" ]; + files = [ + { + file = "/etc/wpa_supplicant.conf"; + how = "symlink"; + } + { + file = "/etc/machine-id"; + inInitrd = true; + } + ]; + users = { + alice.directories = [ ".rabbit_hole" ]; + butz = { + files = [ + { + file = ".config/foo"; + mode = "0600"; + } + "bar" + ]; + directories = [ "unshaved_yaks" ]; + }; + }; + }; + } + ``` + ''; + }; + }; +}