nixos: improve predictability and consistency in directory creation and permissions-setting#97
nixos: improve predictability and consistency in directory creation and permissions-setting#97tomeon wants to merge 25 commits intonix-community:masterfrom
Conversation
|
Hi tomeon! Thank you for putting in all this work - it looks really impressive! It's also a huge changeset, almost all of it in one commit, which makes it harder to review and pinpoint bugs caused by it. Can you please split it up? I would expect one commit per change, e.g: sorting, root option, implicit directories, atomicity and each assertion which isn't directly related to a change. |
|
I came to this PR wondering about permissions myself. Are folks convinced that and Is actually a good API with the complexity involved in sorting and then bubbling up permissions behind the scenes? I'm not personally too convinced it's adding much to the ergonomics to be honest. Instead of... environment.persistence = {
users.me = {
directories = [
# ~/.local is created with ownership implicitly set to me:users
# ~/.local/share is created with ownership implicitly set to me:users
{ directory = ".local/share/nix"; user = "me"; group = "users; }
# ~/.local is not created because "nix" comes before "nvim"
# ~/.local/share is not created because "nix" comes before "nvim"
# (If nix and nvim were swapped, you need to learn about setting ordering priorities)
{ directory = ".local/share/nvim"; user = "nvim"; group = "nvim"; }
];
};
};...maybe it would be more clear for everyone involved if the API did not bubble up permissions? (That is always be explicit, never implicit.) environment.persistence = {
users.me = {
directories = [
".local/share/nix"
# (optional) shorthand for setting permissions inline
{ directory = ".local/share/nvim"; user = "nvim"; group = "nvim"; }
];
permissions = {
# ~/.local is created with default ownership root:root (entirely unaffected by permissions of its children - so files and subdirectories)
# ~/.local/share is created with ownership me:users
".local/share" = { user = "me"; group = "users; };
};
};
};This is perhaps similar to the Alternative naming ideas for Additionally one could potentially have environment.persistence = {
users.me = {
directories = [
".local/share/nix"
# (optional) shorthand for setting permissions inline
{ directory = ".local/share/nvim"; user = "nvim"; group = "nvim"; }
];
# default ownership for ~/.local/share, ~/.local/share/nix is me:users
defaultPermissions = { user = "me"; group = "users"; };
permissions = {
# protect ~/.local (set ownership to root:root)
".local" = { user = "root"; group = "root"; };
};
};
};This is just a thought though, don't let me hold up the PR which already sounds like a nice improvement. |
|
(Alternative naming ideas for |
9553b3b to
48d5bf0
Compare
|
@talyz -- I've split up the changes into what I hope is a reasonably granular sequence. Found and fixed a few typos and other issues on the way, too :). Thank you for prompting me to disaggregate the much-too-big initial commits. There are some commits that might still encompass too much. Examples:
Please LMK if you'd like me to tackle further breaking up these or other commits. Please also note that I've moved the NixOS VM test into a fairly early position in the commit list, but with the bits that check mode and ownership disabled. This is to help ensure that no commits in between the introduction of the test and the commit that enables mode and ownership assertions break directory creation (even though they may break or otherwise alter the directory-permissions-setting logic). The moral of the story: |
|
I also started working on this issue a little while back, since I found the issue interesting, not knowing you had picked it back up. I have now pushed my changes to https://github.com/nix-community/impermanence/tree/dir-creation-order. There is some overlap between our solutions, but the main issue is solved differently. I'm creating directory entries for all parent directories in nix, simplifying the shell code significantly. I'm obviously biased here and would prefer my solution, but there are still things from this PR I would very much like to merge - the tests, extended assertions, fixes, etc, which all look amazing! Maybe we should also sync our efforts going forward - I've created a matrix channel for the project at https://matrix.to/#/#impermanence:nixos.org. |
|
@talyz -- I have some free time coming up and may finally be able to work on rebasing "[t]he tests, extended assertions, fixes, etc." on top of the tip of this project. Not sure how much of it is still relevant; we shall see :). Re the complexity of the shell code in this PR: yes, it is indeed complex, though to some extent I think that's a reflection of the complexity of the problem space. Keeping the path munging in Nix is definitely preferable, though Nix's built-in path handling and the functions available from the |
|
Sorry, I thought I had responded to this already, but apparently not! Anyway, that sounds great! I bet there's still a fair bit of it that's relevant. Yep, that's definitely true. I just prefer to keep the bash complexity to a minimum, even if that means the nix code will be more complex as a result. Complex runtime code just means more work to debug runtime issues users run into. I think your pathname project looks really interesting! I'm all for powerful path handling in nix :) |
rather than individual jobs for each linter
That is, add check that detect that the `nixos.nix` module catches attempts to specify the same output path for multiple directories or files under existing storage (e.g., trying to put both "/abc/same" and "/def/same" into "/same").
That is, test the the `nixos.nix` module catches failure to mark a filesystem as needed for boot when attempting to use it as a persistent storage path.
That is, add a check that verifies that, for every user `<user>` that has files or directories in persistent storage path `<path>`, `config.users.users.<user>.home` matches `environment.persistence.<path>.users.<user>.home`.
This test sets up multiple users and groups, multiple persistent storage directories, and multiple files and directories under those storage directories (some with overlapping destination paths), and then makes assertions about the permissions on the source and destination paths. NOTE that the permissions-related assertions are currently disabled. This is an expedient to allow us to run the check before the intended updates to the directory creation ordering and permissions-setting logic are in place.
with a single `install -d`.
that removes repeated directory separators, removes "." components, and attempts to resolve ".." (and throws if it cannot).
Previous behavior:
splitPath [ "/foo/./bar/." "./bazz/quux/./." ]
#=> [ "foo" "." "bar" "." "." "bazz" "quux" "." "." ]
New behavior:
splitPath [ "/foo/./bar/." "./bazz/quux/./." ]
#=> [ "foo" "bar" "bazz" "quux" ]
As follows:
1. Clean the concatenated paths in `dirListToPath` with `cleanPath`,
2. Make `concatPaths` an alias of `dirListToPath`,
3. Make `splitPath` a small wrapper over `concatPaths` (namely,
split the result of `concatPaths [ list of paths ]` on the
directory separator).
BREAKING CHANGE: all three of `dirListToPath`, `concatPaths`, and
`splitPath` are now liable to throw on ".." elements that cannot be
eliminated lexically.
Adding the internal attributes:
1. `prefix` - a path fragment prepended to `<file>.file` or
`<directory>.dir` when expanding it relative to the persistent
storage directory (exists to support prepending the implicit home
directory in `users.<user>.files` and `users.<user>.directories`
specifications),
2. `relpath` - the file or directory path relative to the persistent
storage path and `/`.
3. `source` - the fully-qualified and normalized path rooted in the
persistent storage path.
4. `destination` - the fully-qualified and normalized path rooted in
`/`.
This consolidates path-expansion logic into the file and directory
specifications themselves, and permits other parts of the Impermanence
module to refer to the expanded paths directly (most frequently,
`destination` and `source`).
to be created/chowned/chmodded using `create-directories.bash`. The directories are sorted such that parent *source* paths (that is, paths under persistent storage directories) are processed prior to child source paths, and parent *destination* paths (that is, paths under the target installation root) are processed prior to child destination paths.
That is, forbid mounts and links whose paths lie under an Impermanence-managed persistent storage path.
BREAKING CHANGE: replace `lib.types.str` with `lib.types.nonEmptyStr` in the type definitions for the `files.<name>.file` and `directories.<name>.directory` options and their corresponding options under `users.<user>`. This prevents specifying the persistent storage directory itself (or `<persistent-storage-directory>/<user-home>`) as a file or directory to be linked or mounted into the installation target.
and add the `hierarchy` option for controlling how directories on the filesystem get created.
Purpose
This PR is intended to make Impermanence's directory creation and permissions-setting logic more predictable and consistent. In brief, the main change consists in topologically sorting
directoryspecifications according to reasonably straightforward and (hopefully) sensible rules, such that users can have a pretty good idea of the order in which Impermanence will create the directories and be able to predict what permissions Impermanence will assign them.Summary of changes
Topological sorting rules
Please see the comments to the
dirBeforefunction for a description of the rules used for orderingdirectoryandparentDirectoryspecifications.The main rule is that parent paths come before child paths. This applies to both "source" paths (i.e., child paths of persistent storage directories) and "destination" paths (the paths where things will be linked or bind-mounted).
The
rootoptionThis PR adds a
rootattribute todirectoryandfilespecifications that indicates the base output path. By default, this is/. This change is not essential to the rest of this PR and could be handled in a separate PR if you would prefer that.Directory creation and permissions-setting
Implicit versus explicit directories
This changeset introduces the notion of an "implicit" directory -- a kind of hidden entry in the
environment.persistence.${persistentStoragePath}.directorieslist. Such hidden entries represent the parent directories (and grandparent directories, etc., all the way back to thepersistentStoragePathitself) of explicitly-specified files and directories.Implicit directories differ from explicit directories in that their associated
mode,user, andgroupsettings are advisory, in the following senses:Among its other effects, the logic described above addresses the fact that, previously, configurations like:
resulted in setting the mode on
/home/meto0755.Atomicity
Given the configuration:
Each component of the source and destination paths (relative to the persistent storage directory and
rootdirectory, respectively) is now created atomically.That is,
/state/foo,/state/foo/bar, and/state/foo/bar/bazare created atomically, as are/foo,/foo/bar, and/foo/bar/baz(note that/stateis not necessarily created atomically).These directories are also effectively
chowned andchmodded atomically, using the trick of creating a temporary directory at a sibling path of the "final" directory,chowning andchmodding that directory, thenrename-ing it to the final path.Additional validations and assertions
Inability to topologically sort directories
If sort order is undecidable, the NixOS module will raise an error mentioning the problem and common causes thereof.
"Recursive" bind mounts and symlinks
The NixOS module now forbids having files and directories bind mounted or symlinked into persistent storage directories. Please see this section of the README for details.
Path traversal
As described in the README, the NixOS module will raise an error if provided any path specifications that contain
..elements that cannot be resolved/eliminated lexically. For instance,foo/../baris fine (it can be simplified to justbarwithout looking at the filesystem and without "escaping" the persistent storage path), but../foo/baris not.Inconsistent permissions settings
This changeset updates the NixOS module to forbid inconsistent permissions settings; please see this bit of the README.
Backward (in)compatibility
Loud breakage
This changeset narrows the space of valid Impermanence configurations. It's likely that the new validations and assertions described above will break someone's NixOS system config. I've tried to make newly-introduced error messages informative enough that users should be able to track down problems in short-ish order, but naturally that's not as nice as being backward-compatible (even if the compatibility is with arguably-subtly-broken configs).
Quiet breakage
It's possible that some users rely on Impermanence's current de facto directory sorting logic, or permissions-setting scheme. The changes in this PR could break such users' setups in ways that may not be immediately apparent. However, because this changeset retains the "don't touch permissions on existing source directories" and "always set the permissions on destination directories to the permissions on the corresponding source directories" logic, my hope is that the surface area for this kind of breakage is fairly small -- for instance, it may only be an issue if standing up a new system where persistent storage directories don't already exist.
Test results
Failing GitHub Actions build with new flake checks, but without changes to the
nixos.nixmodule orcreate-directories.bashscript: here.Passing GitHub Actions build with changes to the
nixos.nixmodule andcreate-directories.bashscript: here.Other considerations
Licensing
The topological sorting implementation is inspired and informed by
fsBeforefrom<nixpkgs>/nixos/lib/utils.nix, which is used for topologically sortingconfig.fileSystems. Though this changeset does not copy code fromfsBefore, the domains and even implementations are similar enough that it may be prudent to citefsBeforeand embed a copy of the MIT license. WDYT?Possible improvements
Add an explicit
createDirectorieslistThis would allow callers to control directory permissions at any and all levels of a persistent storage hierarchy. For instance:
Every entry in
directorieswould have a corresponding (implicit) entry increateDirectoriesfor the directory in question, and every entryfileswould have a corresponding (implicit) entry increateDirectoriesfor the parent directory of the file in question.Thus the final directory scheme for the above would be:
Allow callers to directly define sorting priority
Perhaps it would make sense to add a
priorityattribute to directory specifications that allows callers to tell Impermenance whether a given directory should be created earlier or later than others, along the lines oflib.mkForce,lib.mkDefault, etc.?For instance:
And perhaps, in addition, support
lib.mkForce, etc., directly?:This may be most useful as a tiebreaker in case the directory-sorting comparator cannot otherwise determine which operand comes "before" the other.
Final thoughts
I realize and regret that this PR is not really "first-time contribution to a project" material. It's a big bunch of changes that maintains syntactic compatibility with the existing NixOS module, but definitely not semantic compatibility.
On the other hand, I think the motivation for and results of these changes are sensible and defensible. The directory permissions that lead to testing errors here strike me as surprising and, in some cases, wrong -- particularly the user's home directory having mode
0755.If you are open to working with me to get this over the finish line, I would welcome any and all feedback.
Thank you very much for your consideration.