about summary refs log tree commit diff
path: root/nixos/modules/tasks
diff options
context:
space:
mode:
authornikstur <nikstur@outlook.com>2024-02-03 22:40:40 +0100
committernikstur <nikstur@outlook.com>2024-02-07 22:13:00 +0100
commit1c1cfa073a80354fc2814265f1550bd537c01908 (patch)
tree650836b648e23995e81ad031d23633bf7750b068 /nixos/modules/tasks
parentb87fbb96ad34b559e0c130572b09bc9b49a29b75 (diff)
nixos/filesystems: init overlayfs
Diffstat (limited to 'nixos/modules/tasks')
-rw-r--r--nixos/modules/tasks/filesystems/overlayfs.nix144
1 files changed, 144 insertions, 0 deletions
diff --git a/nixos/modules/tasks/filesystems/overlayfs.nix b/nixos/modules/tasks/filesystems/overlayfs.nix
new file mode 100644
index 0000000000000..e71ef9ba62e9c
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/overlayfs.nix
@@ -0,0 +1,144 @@
+{ config, lib, pkgs, utils, ... }:
+
+let
+  # The scripted initrd contains some magic to add the prefix to the
+  # paths just in time, so we don't add it here.
+  sysrootPrefix = fs:
+    if config.boot.initrd.systemd.enable && (utils.fsNeededForBoot fs) then
+      "/sysroot"
+    else
+      "";
+
+  # Returns a service that creates the required directories before the mount is
+  # created.
+  preMountService = _name: fs:
+    let
+      prefix = sysrootPrefix fs;
+
+      escapedMountpoint = utils.escapeSystemdPath (prefix + fs.mountPoint);
+      mountUnit = "${escapedMountpoint}.mount";
+
+      upperdir = prefix + fs.overlay.upperdir;
+      workdir = prefix + fs.overlay.workdir;
+    in
+    lib.mkIf (fs.overlay.upperdir != null)
+      {
+        "rw-${escapedMountpoint}" = {
+          requiredBy = [ mountUnit ];
+          before = [ mountUnit ];
+          unitConfig = {
+            DefaultDependencies = false;
+            RequiresMountsFor = "${upperdir} ${workdir}";
+          };
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${pkgs.coreutils}/bin/mkdir -p -m 0755 ${upperdir} ${workdir}";
+          };
+        };
+      };
+
+  overlayOpts = { config, ... }: {
+
+    options.overlay = {
+
+      lowerdir = lib.mkOption {
+        type = with lib.types; nullOr (nonEmptyListOf (either str pathInStore));
+        default = null;
+        description = lib.mdDoc ''
+          The list of path(s) to the lowerdir(s).
+
+          To create a writable overlay, you MUST provide an upperdir and a
+          workdir.
+
+          You can create a read-only overlay when you provide multiple (at
+          least 2!) lowerdirs and neither an upperdir nor a workdir.
+        '';
+      };
+
+      upperdir = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = lib.mdDoc ''
+          The path to the upperdir.
+
+          If this is null, a read-only overlay is created using the lowerdir.
+
+          If you set this to some value you MUST also set `workdir`.
+        '';
+      };
+
+      workdir = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = lib.mdDoc ''
+          The path to the workdir.
+
+          This MUST be set if you set `upperdir`.
+        '';
+      };
+
+    };
+
+    config = lib.mkIf (config.overlay.lowerdir != null) {
+      fsType = "overlay";
+      device = lib.mkDefault "overlay";
+
+      options =
+        let
+          prefix = sysrootPrefix config;
+
+          lowerdir = map (s: prefix + s) config.overlay.lowerdir;
+          upperdir = prefix + config.overlay.upperdir;
+          workdir = prefix + config.overlay.workdir;
+        in
+        [
+          "lowerdir=${lib.concatStringsSep ":" lowerdir}"
+        ] ++ lib.optionals (config.overlay.upperdir != null) [
+          "upperdir=${upperdir}"
+          "workdir=${workdir}"
+        ] ++ (map (s: "x-systemd.requires-mounts-for=${s}") lowerdir);
+    };
+
+  };
+in
+
+{
+
+  options = {
+
+    # Merge the overlay options into the fileSystems option.
+    fileSystems = lib.mkOption {
+      type = lib.types.attrsOf (lib.types.submodule [ overlayOpts ]);
+    };
+
+  };
+
+  config =
+    let
+      overlayFileSystems = lib.filterAttrs (_name: fs: (fs.overlay.lowerdir != null)) config.fileSystems;
+      initrdFileSystems = lib.filterAttrs (_name: utils.fsNeededForBoot) overlayFileSystems;
+      userspaceFileSystems = lib.filterAttrs (_name: fs: (!utils.fsNeededForBoot fs)) overlayFileSystems;
+    in
+    {
+
+      boot.initrd.availableKernelModules = lib.mkIf (initrdFileSystems != { }) [ "overlay" ];
+
+      assertions = lib.concatLists (lib.mapAttrsToList
+        (_name: fs: [
+          {
+            assertion = (fs.overlay.upperdir == null) == (fs.overlay.workdir == null);
+            message = "You cannot define a `lowerdir` without a `workdir` and vice versa for mount point: ${fs.mountPoint}";
+          }
+          {
+            assertion = (fs.overlay.lowerdir != null && fs.overlay.upperdir == null) -> (lib.length fs.overlay.lowerdir) >= 2;
+            message = "A read-only overlay (without an `upperdir`) requires at least 2 `lowerdir`s: ${fs.mountPoint}";
+          }
+        ])
+        config.fileSystems);
+
+      boot.initrd.systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService initrdFileSystems);
+      systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService userspaceFileSystems);
+
+    };
+
+}