2022-12-29 16:38:58 +00:00
|
|
|
# borrows from:
|
|
|
|
# https://xeiaso.net/blog/paranoid-nixos-2021-07-18
|
|
|
|
# https://elis.nu/blog/2020/05/nixos-tmpfs-as-root/
|
|
|
|
# https://github.com/nix-community/impermanence
|
2023-01-03 14:55:27 +00:00
|
|
|
{ config, lib, pkgs, utils, sane-lib, ... }:
|
2022-12-29 16:38:58 +00:00
|
|
|
|
|
|
|
with lib;
|
|
|
|
let
|
2023-01-03 14:55:27 +00:00
|
|
|
path = sane-lib.path;
|
2023-01-04 00:59:52 +00:00
|
|
|
sane-types = sane-lib.types;
|
2023-01-06 10:04:51 +00:00
|
|
|
cfg = config.sane.persist;
|
2023-01-03 03:04:17 +00:00
|
|
|
|
2023-01-03 07:04:49 +00:00
|
|
|
storeType = types.submodule {
|
|
|
|
options = {
|
2023-01-04 01:54:13 +00:00
|
|
|
storeDescription = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
an optional description of the store, which is rendered like
|
|
|
|
{store.name}: {store.storeDescription}
|
|
|
|
for example, a store named "private" could have description "ecnrypted to the user's password and decrypted on login".
|
|
|
|
'';
|
|
|
|
};
|
2023-01-04 12:19:32 +00:00
|
|
|
origin = mkOption {
|
2023-01-03 07:04:49 +00:00
|
|
|
type = types.str;
|
2024-02-23 04:01:19 +00:00
|
|
|
description = ''
|
|
|
|
where this store is rooted within the outer filesystem.
|
|
|
|
conceptually, this is the store's "mount point",
|
|
|
|
though technically this could be a path within any larger mount.
|
|
|
|
'';
|
2023-01-03 07:04:49 +00:00
|
|
|
};
|
|
|
|
prefix = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "/";
|
2023-01-03 07:45:19 +00:00
|
|
|
description = ''
|
|
|
|
optional prefix to strip from children when stored here.
|
|
|
|
for example, prefix="/var/private" and mountpoint="/mnt/crypt/private"
|
|
|
|
would cause /var/private/www/root to be stored at /mnt/crypt/private/www/root instead of
|
|
|
|
/mnt/crypt/private/var/private/www/root.
|
|
|
|
'';
|
2023-01-03 07:04:49 +00:00
|
|
|
};
|
2023-01-06 14:44:32 +00:00
|
|
|
defaultMethod = mkOption {
|
|
|
|
type = types.enum [ "bind" "symlink" ];
|
2024-02-23 03:36:31 +00:00
|
|
|
default = "symlink";
|
2023-01-06 14:44:32 +00:00
|
|
|
description = ''
|
|
|
|
preferred way to link items from the store into the fs
|
|
|
|
'';
|
|
|
|
};
|
2023-01-04 11:22:26 +00:00
|
|
|
defaultOrdering.wantedBeforeBy = mkOption {
|
2023-01-03 07:04:49 +00:00
|
|
|
type = types.listOf types.str;
|
2023-01-04 11:22:26 +00:00
|
|
|
default = [ "local-fs.target" ];
|
2023-01-03 07:45:19 +00:00
|
|
|
description = ''
|
2023-01-04 11:22:26 +00:00
|
|
|
list of units or targets which would prefer that everything in this store
|
|
|
|
be initialized before they run, but failing to do so should not error the items in this list.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
defaultOrdering.wantedBy = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [ ];
|
|
|
|
description = ''
|
|
|
|
list of units or targets which, upon activation, should activate all units in this store.
|
2023-01-03 07:45:19 +00:00
|
|
|
'';
|
2023-01-03 07:04:49 +00:00
|
|
|
};
|
|
|
|
};
|
2023-01-03 03:04:17 +00:00
|
|
|
};
|
2022-12-29 16:38:58 +00:00
|
|
|
|
2023-01-06 12:28:55 +00:00
|
|
|
# allows a user to specify the store either by name or as an attrset
|
2023-01-06 11:56:22 +00:00
|
|
|
coercedToStore = types.coercedTo types.str (s: cfg.stores."${s}") storeType;
|
|
|
|
|
2023-01-06 13:58:36 +00:00
|
|
|
# options common to all entries, whether they're keyed by path or store
|
|
|
|
entryOpts = {
|
2022-12-29 16:38:58 +00:00
|
|
|
options = {
|
2023-01-06 13:47:59 +00:00
|
|
|
acl = mkOption {
|
|
|
|
type = sane-types.aclOverride;
|
|
|
|
default = {};
|
|
|
|
};
|
2023-01-06 14:05:49 +00:00
|
|
|
method = mkOption {
|
2023-01-06 14:44:32 +00:00
|
|
|
type = types.nullOr (types.enum [ "bind" "symlink" ]);
|
|
|
|
default = null;
|
2023-01-06 14:05:49 +00:00
|
|
|
description = ''
|
|
|
|
how to link the store entry into the fs
|
|
|
|
'';
|
|
|
|
};
|
2023-07-08 00:56:20 +00:00
|
|
|
type = mkOption {
|
|
|
|
type = types.enum [ "dir" "file" ];
|
|
|
|
default = "dir";
|
|
|
|
description = ''
|
|
|
|
whether the thing being persisted is a whole directory,
|
|
|
|
or just one file.
|
|
|
|
'';
|
|
|
|
};
|
2022-12-29 16:38:58 +00:00
|
|
|
};
|
|
|
|
};
|
2023-01-06 13:58:36 +00:00
|
|
|
|
|
|
|
# options for a single mountpoint / persistence where the store is specified externally
|
|
|
|
entryInStore = types.submodule [
|
|
|
|
entryOpts
|
|
|
|
{
|
|
|
|
options = {
|
2023-07-08 00:56:20 +00:00
|
|
|
path = mkOption {
|
2023-01-06 13:58:36 +00:00
|
|
|
type = types.str;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
];
|
2023-07-08 00:56:20 +00:00
|
|
|
# allow "bar/baz" as shorthand for { path = "bar/baz"; }
|
2023-01-06 12:28:55 +00:00
|
|
|
entryInStoreOrShorthand = types.coercedTo
|
2023-01-03 07:04:49 +00:00
|
|
|
types.str
|
2023-07-08 00:56:20 +00:00
|
|
|
(d: { path = d; })
|
2023-01-06 12:28:55 +00:00
|
|
|
entryInStore;
|
2022-12-29 16:38:58 +00:00
|
|
|
|
2023-01-06 13:47:59 +00:00
|
|
|
# allow the user to provide the `acl` field inline: we pop acl sub-attributes placed at the
|
|
|
|
# toplevel and move them into an `acl` attribute.
|
|
|
|
convertInlineAcl = to: types.coercedTo
|
|
|
|
types.attrs
|
2023-01-09 03:11:14 +00:00
|
|
|
(orig: lib.recursiveUpdate
|
|
|
|
(builtins.removeAttrs orig ["user" "group" "mode" ])
|
|
|
|
{
|
2023-01-13 01:50:08 +00:00
|
|
|
acl = sane-lib.filterByName ["user" "group" "mode"] orig;
|
2023-01-09 03:11:14 +00:00
|
|
|
}
|
|
|
|
)
|
2023-01-06 13:47:59 +00:00
|
|
|
to;
|
|
|
|
|
2023-01-06 12:28:55 +00:00
|
|
|
# entry where the path is specified externally
|
2023-01-06 13:58:36 +00:00
|
|
|
entryAtPath = types.submodule [
|
|
|
|
entryOpts
|
|
|
|
{
|
|
|
|
options = {
|
|
|
|
store = mkOption {
|
|
|
|
type = coercedToStore;
|
|
|
|
};
|
2023-01-06 11:52:28 +00:00
|
|
|
};
|
2023-01-06 13:58:36 +00:00
|
|
|
}
|
|
|
|
];
|
2023-01-06 11:52:28 +00:00
|
|
|
|
2023-11-08 15:32:50 +00:00
|
|
|
# this submodule converts store-based access to path-based access so that the user can specify e.g.:
|
|
|
|
# <top>.byStore.private = [ ".cache/vim" ];
|
|
|
|
# <top>.byStore.private = [ { path=".cache/vim"; mode = "0700"; } ];
|
|
|
|
# to place ".cache/vim" into the private store and/or create with the appropriate mode
|
2023-07-08 00:56:20 +00:00
|
|
|
entrySubmodule = types.submodule ({ config, ... }: {
|
2023-11-08 15:32:50 +00:00
|
|
|
options = {
|
|
|
|
byStore = mkOption {
|
|
|
|
type = types.attrsOf (types.listOf (convertInlineAcl entryInStoreOrShorthand));
|
|
|
|
default = {};
|
|
|
|
description = ''
|
|
|
|
directories/files to persist within a specific store (e.g. "plaintext" or "private").
|
|
|
|
'';
|
2023-01-06 13:06:39 +00:00
|
|
|
};
|
2023-11-08 15:32:50 +00:00
|
|
|
byPath = mkOption {
|
|
|
|
type = types.attrsOf (convertInlineAcl entryAtPath);
|
|
|
|
default = {};
|
|
|
|
description = ''
|
|
|
|
map of <path> => <path config> for all paths to be persisted.
|
|
|
|
this is computed from the other options, but users can also set it explicitly (useful for overriding)
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
2023-01-06 13:06:39 +00:00
|
|
|
config = let
|
|
|
|
# set the `store` attribute on one dir attrset
|
2023-01-06 14:20:30 +00:00
|
|
|
annotateWithStore = store: dir: {
|
2023-07-08 00:56:20 +00:00
|
|
|
"${dir.path}".store = store;
|
2023-01-06 13:06:39 +00:00
|
|
|
};
|
2023-01-06 14:20:30 +00:00
|
|
|
# convert an `entryInStore` to an `entryAtPath` (less the `store` item)
|
2023-01-06 13:06:39 +00:00
|
|
|
dirToAttrs = dir: {
|
2023-07-08 00:56:20 +00:00
|
|
|
"${dir.path}" = builtins.removeAttrs dir ["path"];
|
2023-01-06 13:06:39 +00:00
|
|
|
};
|
2023-01-06 14:20:30 +00:00
|
|
|
store-names = attrNames cfg.stores;
|
|
|
|
# :: (store -> entry -> AttrSet) -> [AttrSet]
|
2023-11-08 15:32:50 +00:00
|
|
|
# applyToAllStores = f: lib.concatMap
|
|
|
|
# (store: map (f store) config.byStore."${store}")
|
|
|
|
# store-names;
|
2023-01-06 14:20:30 +00:00
|
|
|
applyToAllStores = f: lib.concatMap
|
2023-11-08 15:32:50 +00:00
|
|
|
(store: map (f store) config.byStore."${store}")
|
|
|
|
(builtins.attrNames config.byStore);
|
2023-01-06 13:06:39 +00:00
|
|
|
in {
|
2023-01-06 14:20:30 +00:00
|
|
|
byPath = lib.mkMerge (concatLists [
|
2023-01-27 00:00:50 +00:00
|
|
|
# convert the list-style per-store entries into attrsOf entries
|
2023-11-08 15:32:50 +00:00
|
|
|
(applyToAllStores (_store: dirToAttrs))
|
2023-01-27 00:00:50 +00:00
|
|
|
# add the `store` attr to everything we ingested
|
2023-01-06 14:20:30 +00:00
|
|
|
(applyToAllStores annotateWithStore)
|
|
|
|
]);
|
2023-01-06 13:06:39 +00:00
|
|
|
};
|
|
|
|
});
|
2022-12-29 16:38:58 +00:00
|
|
|
in
|
|
|
|
{
|
|
|
|
options = {
|
2023-01-06 10:04:51 +00:00
|
|
|
sane.persist.enable = mkOption {
|
2022-12-29 16:38:58 +00:00
|
|
|
default = false;
|
|
|
|
type = types.bool;
|
|
|
|
};
|
2023-01-06 11:29:13 +00:00
|
|
|
sane.persist.sys = mkOption {
|
2023-07-08 00:56:20 +00:00
|
|
|
description = "directories (or files) to persist to disk, relative to the fs root /";
|
2023-01-03 07:04:49 +00:00
|
|
|
default = {};
|
2023-07-08 00:56:20 +00:00
|
|
|
type = entrySubmodule;
|
2023-01-03 07:04:49 +00:00
|
|
|
};
|
2023-01-06 10:04:51 +00:00
|
|
|
sane.persist.stores = mkOption {
|
2023-01-03 07:04:49 +00:00
|
|
|
type = types.attrsOf storeType;
|
|
|
|
default = {};
|
2023-01-03 07:45:19 +00:00
|
|
|
description = ''
|
|
|
|
map from human-friendly name to a fs sub-tree from which files are linked into the logical fs.
|
|
|
|
'';
|
2022-12-31 09:09:51 +00:00
|
|
|
};
|
2022-12-29 16:38:58 +00:00
|
|
|
};
|
|
|
|
|
2022-12-30 04:35:34 +00:00
|
|
|
imports = [
|
2023-01-03 07:04:49 +00:00
|
|
|
./stores
|
2022-12-30 04:35:34 +00:00
|
|
|
];
|
2022-12-29 16:38:58 +00:00
|
|
|
|
2023-01-03 08:25:43 +00:00
|
|
|
config = let
|
2023-07-08 00:56:20 +00:00
|
|
|
# String => entryAtPath => generated toplevel config
|
2023-01-06 11:52:28 +00:00
|
|
|
cfgFor = fspath: opt:
|
2023-01-03 08:25:43 +00:00
|
|
|
let
|
|
|
|
store = opt.store;
|
2023-01-06 14:44:32 +00:00
|
|
|
method = (sane-lib.withDefault store.defaultMethod) opt.method;
|
2023-01-06 09:56:06 +00:00
|
|
|
fsPathToStoreRelPath = fspath: path.from store.prefix fspath;
|
|
|
|
fsPathToBackingPath = fspath: path.concat [ store.origin (fsPathToStoreRelPath fspath) ];
|
2023-01-09 11:14:59 +00:00
|
|
|
in lib.mkMerge [
|
2023-01-06 09:56:06 +00:00
|
|
|
{
|
|
|
|
# create destination dir, with correct perms
|
2023-01-06 11:52:28 +00:00
|
|
|
sane.fs."${fspath}" = {
|
2023-01-06 14:05:49 +00:00
|
|
|
inherit (store.defaultOrdering) wantedBy wantedBeforeBy;
|
2023-01-06 14:44:32 +00:00
|
|
|
} // (lib.optionalAttrs (method == "bind") {
|
2023-01-06 09:56:06 +00:00
|
|
|
# inherit perms & make sure we don't mount until after the mount point is setup correctly.
|
2023-01-06 13:47:59 +00:00
|
|
|
dir.acl = opt.acl;
|
2023-01-06 11:52:28 +00:00
|
|
|
mount.bind = fsPathToBackingPath fspath;
|
2023-01-06 14:44:32 +00:00
|
|
|
}) // (lib.optionalAttrs (method == "symlink") {
|
2023-01-06 14:05:49 +00:00
|
|
|
symlink.acl = opt.acl;
|
|
|
|
symlink.target = fsPathToBackingPath fspath;
|
|
|
|
});
|
2023-01-06 09:56:06 +00:00
|
|
|
}
|
2023-07-08 00:56:20 +00:00
|
|
|
(lib.optionalAttrs (opt.type == "dir") {
|
|
|
|
# create the backing path as a dir
|
2023-07-08 02:08:18 +00:00
|
|
|
sane.fs."${fsPathToBackingPath fspath}" = {
|
|
|
|
wantedBeforeBy = [ config.sane.fs."${fspath}".unit ];
|
|
|
|
dir.acl = config.sane.fs."${fspath}".generated.acl;
|
2023-07-08 00:56:20 +00:00
|
|
|
};
|
|
|
|
})
|
|
|
|
(lib.optionalAttrs (opt.type == "file") {
|
|
|
|
# ensure the backing path of this file's parent exists.
|
|
|
|
# XXX: this forces the backing parent to be a directory
|
|
|
|
# this is almost always what is wanted, but it's sometimes an arbitrary constraint
|
2023-07-08 02:08:18 +00:00
|
|
|
sane.fs."${path.parent (fsPathToBackingPath fspath)}" = {
|
|
|
|
wantedBeforeBy = [ config.sane.fs."${fspath}".unit ];
|
|
|
|
dir = {};
|
|
|
|
};
|
2023-07-08 00:56:20 +00:00
|
|
|
})
|
2023-01-06 09:56:06 +00:00
|
|
|
{
|
|
|
|
# default each item along the backing path to have the same acl as the location it would be mounted.
|
2023-01-09 03:48:07 +00:00
|
|
|
sane.fs = lib.mkMerge (builtins.map
|
|
|
|
(fsSubpath: {
|
|
|
|
"${fsPathToBackingPath fsSubpath}" = {
|
|
|
|
generated.acl = config.sane.fs."${fsSubpath}".generated.acl;
|
|
|
|
};
|
|
|
|
})
|
2023-07-08 00:56:20 +00:00
|
|
|
(path.walk store.prefix (path.parent fspath))
|
2023-01-09 03:48:07 +00:00
|
|
|
);
|
2023-01-06 09:56:06 +00:00
|
|
|
}
|
|
|
|
];
|
2023-01-30 10:51:41 +00:00
|
|
|
configs = lib.mapAttrsToList cfgFor cfg.sys.byPath;
|
2023-01-09 09:42:17 +00:00
|
|
|
take = f: { sane.fs = f.sane.fs; };
|
|
|
|
in mkIf cfg.enable (
|
|
|
|
take (sane-lib.mkTypedMerge take configs)
|
|
|
|
);
|
2022-12-29 16:38:58 +00:00
|
|
|
}
|
|
|
|
|