refactor(guest): simplify guest options
This commit is contained in:
552
flake/guest.nix
552
flake/guest.nix
@@ -8,355 +8,285 @@
|
|||||||
util,
|
util,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
mkGuest =
|
mkGuestOptions =
|
||||||
{
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
name,
|
name,
|
||||||
interior ? { },
|
|
||||||
}:
|
}:
|
||||||
{ config, lib, ... }:
|
{
|
||||||
let
|
enable = lib.mkEnableOption "the ${name} guest, run in its own nested container";
|
||||||
optionPath = [ "guests" ] ++ lib.splitString "." name;
|
vlan = lib.mkOption {
|
||||||
cfg = lib.getAttrFromPath optionPath config;
|
type = lib.types.ints.between 1 4094;
|
||||||
machineName = lib.replaceStrings [ "." ] [ "-" ] name;
|
example = 10;
|
||||||
|
description = ''
|
||||||
networked = cfg.vlan != null;
|
The tagged VLAN this guest lives on. The guest attaches to its host's
|
||||||
|
`br-vlan<id>` bridge for that VLAN. The id must be one of the host's
|
||||||
userMounts = lib.mapAttrs (_guestPath: m: {
|
`modules.network.vlans.ids`.
|
||||||
inherit (m) hostPath;
|
'';
|
||||||
isReadOnly = m.readOnly;
|
|
||||||
}) cfg.mounts;
|
|
||||||
|
|
||||||
secretMounts = lib.listToAttrs (
|
|
||||||
map (
|
|
||||||
name:
|
|
||||||
let
|
|
||||||
path = config.sops.secrets.${name}.path;
|
|
||||||
in
|
|
||||||
lib.nameValuePair path {
|
|
||||||
hostPath = path;
|
|
||||||
isReadOnly = true;
|
|
||||||
}
|
|
||||||
) cfg.secrets
|
|
||||||
);
|
|
||||||
|
|
||||||
mountCollisions = lib.attrNames (builtins.intersectAttrs userMounts secretMounts);
|
|
||||||
|
|
||||||
limitConfig = lib.filterAttrs (_: v: v != null) {
|
|
||||||
MemoryMax = cfg.limits.memory;
|
|
||||||
CPUQuota = cfg.limits.cpu;
|
|
||||||
TasksMax = cfg.limits.tasksMax;
|
|
||||||
};
|
};
|
||||||
|
mac = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = util.deriveMac "${config.networking.hostName}.${name}";
|
||||||
|
defaultText = lib.literalMD "a stable address derived from the host name and guest option path";
|
||||||
|
example = "bc:24:11:00:00:01";
|
||||||
|
description = ''
|
||||||
|
The guest's MAC address on its VLAN, pinned inside the guest by its
|
||||||
|
own networkd. Set it to reuse an existing address so a router's DHCP
|
||||||
|
reservation keeps working. Left unset, a stable address is derived
|
||||||
|
from the host name and guest option path in the locally-administered range.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
address = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "10.23.20.126";
|
||||||
|
description = ''
|
||||||
|
Expected IP address assigned outside NixOS for this guest.
|
||||||
|
Left null, this guest contributes no address to the address registry.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
reverseProxy = {
|
||||||
|
enable = lib.mkEnableOption "a reverse-proxy route for this guest";
|
||||||
|
|
||||||
createdMounts = lib.filterAttrs (_guestPath: m: m.create != null) cfg.mounts;
|
route = lib.mkOption {
|
||||||
mountCreateScript = lib.concatStringsSep "\n" (
|
type = registryAssembly.definitions.route.type;
|
||||||
|
description = "Route registry entry published for this guest.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
mounts = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
directory = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/media";
|
||||||
|
description = "Host directory bind-mounted into the guest.";
|
||||||
|
};
|
||||||
|
file = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/run/secrets/service-password";
|
||||||
|
description = "Host file bind-mounted into the guest.";
|
||||||
|
};
|
||||||
|
readOnly = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Mount the host path read-only inside the guest.";
|
||||||
|
};
|
||||||
|
owner = lib.mkOption {
|
||||||
|
type = lib.types.nullOr (
|
||||||
|
lib.types.oneOf [
|
||||||
|
lib.types.str
|
||||||
|
lib.types.ints.positive
|
||||||
|
]
|
||||||
|
);
|
||||||
|
default = null;
|
||||||
|
description = "Owner assigned to the host directory before the guest starts.";
|
||||||
|
};
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Group assigned to the host directory before the guest starts.";
|
||||||
|
};
|
||||||
|
mode = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "0770";
|
||||||
|
description = "Mode assigned to the host directory before the guest starts.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
"/data/media" = { directory = "/srv/media"; };
|
||||||
|
"/data/config" = { directory = "/srv/config/jellyfin"; };
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
Host paths bind-mounted into the guest, keyed by the path they appear
|
||||||
|
at inside the guest, so a guest sees exactly the data it should at any
|
||||||
|
granularity — a single folder, a whole pool, or a decrypted secret.
|
||||||
|
Mounts are read-write unless `readOnly` is set.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
limits = {
|
||||||
|
memory = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "2G";
|
||||||
|
description = ''
|
||||||
|
Cap on the guest's memory, applied to its unit as `MemoryMax`.
|
||||||
|
Accepts systemd size suffixes such as `512M` or `2G`. Left null,
|
||||||
|
the guest's memory is uncapped.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
cpu = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "150%";
|
||||||
|
description = ''
|
||||||
|
Cap on the guest's CPU, applied to its unit as `CPUQuota`, where
|
||||||
|
`100%` is one full core. Left null, the guest's CPU is uncapped.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
tasksMax = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.ints.positive;
|
||||||
|
default = null;
|
||||||
|
example = 512;
|
||||||
|
description = ''
|
||||||
|
Cap on the number of processes and threads the guest may spawn,
|
||||||
|
applied to its unit as `TasksMax`. Left null, the task count is
|
||||||
|
uncapped.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mkGuestConfig =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
interior,
|
||||||
|
lib,
|
||||||
|
name,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.guests.${name};
|
||||||
|
|
||||||
|
directoryMounts = lib.filterAttrs (_: mount: mount.directory != null) cfg.mounts;
|
||||||
|
|
||||||
|
directoryScript = lib.concatStringsSep "\n" (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
_guestPath: m:
|
_: mount:
|
||||||
let
|
|
||||||
create = m.create;
|
|
||||||
in
|
|
||||||
''
|
''
|
||||||
${config.system.path}/bin/install -d -o ${lib.escapeShellArg (toString create.owner)} -g ${lib.escapeShellArg create.group} -m ${lib.escapeShellArg create.mode} ${lib.escapeShellArg m.hostPath}
|
${config.system.path}/bin/install -d ${lib.optionalString (mount.owner != null) "-o ${lib.escapeShellArg (toString mount.owner)}"} ${lib.optionalString (mount.group != null) "-g ${lib.escapeShellArg mount.group}"} ${lib.optionalString (mount.mode != null) "-m ${lib.escapeShellArg mount.mode}"} ${lib.escapeShellArg mount.directory}
|
||||||
''
|
''
|
||||||
) createdMounts
|
) directoryMounts
|
||||||
);
|
);
|
||||||
|
|
||||||
guestNet =
|
guestNet =
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
config = lib.mkIf networked {
|
networking.useNetworkd = true;
|
||||||
networking.useNetworkd = true;
|
networking.useHostResolvConf = false;
|
||||||
networking.useHostResolvConf = false;
|
|
||||||
|
|
||||||
systemd.network.networks."20-eth0" = {
|
systemd.network.networks."20-eth0" = {
|
||||||
matchConfig.Name = "eth0";
|
matchConfig.Name = "eth0";
|
||||||
linkConfig.MACAddress = cfg.mac;
|
linkConfig.MACAddress = cfg.mac;
|
||||||
networkConfig.DHCP = "yes";
|
networkConfig.DHCP = "yes";
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
lib.mkIf cfg.enable {
|
||||||
options = lib.setAttrByPath optionPath {
|
registry.address.${config.networking.hostName}.${name} = lib.mkIf (cfg.address != null) cfg.address;
|
||||||
enable = lib.mkEnableOption "the ${name} guest, run in its own nested container";
|
|
||||||
backend = lib.mkOption {
|
|
||||||
type = lib.types.enum [
|
|
||||||
"container"
|
|
||||||
"microvm"
|
|
||||||
];
|
|
||||||
default = "container";
|
|
||||||
description = ''
|
|
||||||
How the guest is realized. `container` runs the guest as a
|
|
||||||
systemd-nspawn nested container. `microvm` is reserved for a future
|
|
||||||
hard-isolation backend and is not built yet.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
vlan = lib.mkOption {
|
|
||||||
type = lib.types.nullOr (lib.types.ints.between 1 4094);
|
|
||||||
default = null;
|
|
||||||
example = 10;
|
|
||||||
description = ''
|
|
||||||
The tagged VLAN this guest lives on. The guest attaches to its host's
|
|
||||||
`br-vlan<id>` bridge for that VLAN. Left null, the guest keeps a
|
|
||||||
private network with no bridge attachment. The id must be one of the
|
|
||||||
host's `modules.network.vlans.ids`.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
mac = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = util.deriveMac name;
|
|
||||||
defaultText = lib.literalMD "a stable address derived from the guest's namespace path";
|
|
||||||
example = "bc:24:11:00:00:01";
|
|
||||||
description = ''
|
|
||||||
The guest's MAC address on its VLAN, pinned inside the guest by its
|
|
||||||
own networkd. Set it to reuse an existing address so a router's DHCP
|
|
||||||
reservation keeps working. Left unset, a stable address is derived
|
|
||||||
from the guest's namespace path in the locally-administered range.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
address = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "10.23.20.126";
|
|
||||||
description = ''
|
|
||||||
Expected IP address assigned outside NixOS for this guest.
|
|
||||||
Left null, this guest contributes no address to the address registry.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
reverseProxy = {
|
|
||||||
enable = lib.mkEnableOption "a reverse-proxy route for this guest";
|
|
||||||
|
|
||||||
route = lib.mkOption {
|
registry.route.${config.networking.hostName}.${name} = lib.mkIf cfg.reverseProxy.enable cfg.reverseProxy.route;
|
||||||
type = registryAssembly.definitions.route.type;
|
|
||||||
description = "Route registry entry published for this guest.";
|
assertions =
|
||||||
};
|
let
|
||||||
};
|
hasDirectorySettings = mount: mount.owner != null || mount.group != null || mount.mode != null;
|
||||||
mounts = lib.mkOption {
|
hasCompleteDirectorySettings = mount: mount.owner != null && mount.group != null && mount.mode != null;
|
||||||
type = lib.types.attrsOf (
|
hasOneSource = mount: (mount.directory != null) != (mount.file != null);
|
||||||
lib.types.submodule {
|
mountsWithoutOneSource = lib.attrNames (lib.filterAttrs (_: mount: !hasOneSource mount) cfg.mounts);
|
||||||
options = {
|
fileMountsWithDirectorySettings = lib.attrNames (
|
||||||
hostPath = lib.mkOption {
|
lib.filterAttrs (_: mount: mount.file != null && hasDirectorySettings mount) cfg.mounts
|
||||||
type = lib.types.str;
|
|
||||||
example = "/srv/media";
|
|
||||||
description = "The path on the host bind-mounted into the guest.";
|
|
||||||
};
|
|
||||||
readOnly = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = ''
|
|
||||||
Mount the path read-only. Read-write by default, since a
|
|
||||||
service must write to the pool data it owns.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
create = lib.mkOption {
|
|
||||||
type = lib.types.nullOr (lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
type = lib.mkOption {
|
|
||||||
type = lib.types.enum [ "directory" ];
|
|
||||||
default = "directory";
|
|
||||||
description = "Host path type to create before the guest starts.";
|
|
||||||
};
|
|
||||||
owner = lib.mkOption {
|
|
||||||
type = lib.types.oneOf [
|
|
||||||
lib.types.str
|
|
||||||
lib.types.ints.positive
|
|
||||||
];
|
|
||||||
description = "Owner assigned to the created host path.";
|
|
||||||
};
|
|
||||||
group = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Group assigned to the created host path.";
|
|
||||||
};
|
|
||||||
mode = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "0770";
|
|
||||||
description = "Mode assigned to the created host path.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
});
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
Opt-in host path creation before the guest starts. Existing
|
|
||||||
paths are left enabled unless this is set, so served storage
|
|
||||||
roots can still be required to exist explicitly.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
default = { };
|
incompleteHostDirectoryMounts = lib.attrNames (
|
||||||
example = lib.literalExpression ''
|
lib.filterAttrs (_: mount: mount.directory != null && hasDirectorySettings mount && !hasCompleteDirectorySettings mount) cfg.mounts
|
||||||
{
|
);
|
||||||
"/data/media" = { hostPath = "/srv/media"; };
|
in
|
||||||
"/data/config" = {
|
[
|
||||||
hostPath = "/srv/config/jellyfin";
|
|
||||||
readOnly = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
description = ''
|
|
||||||
Host paths bind-mounted into the guest, keyed by the path they appear
|
|
||||||
at inside the guest, so a guest sees exactly the data it should at any
|
|
||||||
granularity — a single folder or a whole pool. Each mount is
|
|
||||||
read-write unless `readOnly` is set.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
secrets = lib.mkOption {
|
|
||||||
type = lib.types.listOf lib.types.str;
|
|
||||||
default = [ ];
|
|
||||||
example = [ "jellyfin-api-key" ];
|
|
||||||
description = ''
|
|
||||||
Names of the secrets this guest needs. The host is the sole
|
|
||||||
decryptor: it decrypts each named secret from its own sops files and
|
|
||||||
bind-mounts the plaintext file into the guest read-only at
|
|
||||||
`/run/secrets/<name>`, the same path it would occupy on a host, so a
|
|
||||||
service reads its credentials at a predictable location. The guest
|
|
||||||
names the files it wants and receives exactly those. It holds no age
|
|
||||||
key and decrypts nothing itself. Ownership carries across unchanged,
|
|
||||||
since the container maps ids one to one, so a secret owned by a uid on
|
|
||||||
the host is owned by that same uid inside the guest.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
limits = {
|
|
||||||
memory = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "2G";
|
|
||||||
description = ''
|
|
||||||
Cap on the guest's memory, applied to its unit as `MemoryMax`.
|
|
||||||
Accepts systemd size suffixes such as `512M` or `2G`. Left null,
|
|
||||||
the guest's memory is uncapped.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
cpu = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "150%";
|
|
||||||
description = ''
|
|
||||||
Cap on the guest's CPU, applied to its unit as `CPUQuota`, where
|
|
||||||
`100%` is one full core. Left null, the guest's CPU is uncapped.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
tasksMax = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.ints.positive;
|
|
||||||
default = null;
|
|
||||||
example = 512;
|
|
||||||
description = ''
|
|
||||||
Cap on the number of processes and threads the guest may spawn,
|
|
||||||
applied to its unit as `TasksMax`. Left null, the task count is
|
|
||||||
uncapped.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
nesting = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = ''
|
|
||||||
Grant the guest's interior the prerequisites to run Podman or other
|
|
||||||
OCI containers of its own. Off by default, so a guest cannot nest
|
|
||||||
containers. On, the guest's container gains the network-administration
|
|
||||||
capability its container runtime uses to build bridges and firewall
|
|
||||||
rules, along with the tun and fuse device nodes such a runtime reaches
|
|
||||||
for, so the interior's `virtualisation.oci-containers` works with
|
|
||||||
Podman as its default runtime.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
autoStart = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
Start the guest at boot. On by default. Disabled, the guest stays
|
|
||||||
defined and can be started on demand, but does not come up at boot.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
sops.secrets = lib.genAttrs cfg.secrets (_: { });
|
|
||||||
|
|
||||||
registry.address.${config.networking.hostName}.${machineName} = lib.mkIf (cfg.address != null) cfg.address;
|
|
||||||
|
|
||||||
registry.route.${config.networking.hostName}.${machineName} = lib.mkIf cfg.reverseProxy.enable cfg.reverseProxy.route;
|
|
||||||
|
|
||||||
assertions = [
|
|
||||||
{
|
{
|
||||||
assertion = mountCollisions == [ ];
|
assertion = mountsWithoutOneSource == [ ];
|
||||||
message = ''
|
message = ''
|
||||||
guests.${name} maps a mount at ${lib.concatStringsSep ", " mountCollisions}, colliding with a secret bind-mounted at the same path. Rename the mount or the secret so each in-guest path is used once.
|
guests.${name} mounts must set exactly one of directory or file for ${lib.concatStringsSep ", " mountsWithoutOneSource}.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion = cfg.backend == "container";
|
assertion = fileMountsWithDirectorySettings == [ ];
|
||||||
message = ''
|
message = ''
|
||||||
guests.${name}.backend = "${cfg.backend}" is not implemented. Only the "container" backend is built; "microvm" is reserved for future work.
|
guests.${name} file mounts must not set owner, group, or mode for ${lib.concatStringsSep ", " fileMountsWithDirectorySettings}.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion = !networked || lib.elem cfg.vlan config.modules.network.vlans.ids;
|
assertion = incompleteHostDirectoryMounts == [ ];
|
||||||
|
message = ''
|
||||||
|
guests.${name} has incomplete host directory settings for ${lib.concatStringsSep ", " incompleteHostDirectoryMounts}. Set owner, group, and mode together.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = lib.elem cfg.vlan config.modules.network.vlans.ids;
|
||||||
message = ''
|
message = ''
|
||||||
guests.${name}.vlan = ${toString cfg.vlan} is not among its host's modules.network.vlans.ids (${lib.concatMapStringsSep ", " toString config.modules.network.vlans.ids}). Declare the VLAN on the host or correct the guest's placement.
|
guests.${name}.vlan = ${toString cfg.vlan} is not among its host's modules.network.vlans.ids (${lib.concatMapStringsSep ", " toString config.modules.network.vlans.ids}). Declare the VLAN on the host or correct the guest's placement.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services."container@${machineName}" = lib.mkIf (cfg.backend == "container") (
|
systemd.services."container@${name}" = lib.mkMerge [
|
||||||
lib.mkMerge [
|
{
|
||||||
{
|
serviceConfig = lib.filterAttrs (_: v: v != null) {
|
||||||
serviceConfig = limitConfig;
|
MemoryMax = cfg.limits.memory;
|
||||||
preStart = lib.mkIf (createdMounts != { }) mountCreateScript;
|
CPUQuota = cfg.limits.cpu;
|
||||||
}
|
TasksMax = cfg.limits.tasksMax;
|
||||||
(lib.mkIf networked (
|
|
||||||
let
|
|
||||||
bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (util.bridgeName cfg.vlan)}.device";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
after = [ bridgeDevice ];
|
|
||||||
wants = [ bridgeDevice ];
|
|
||||||
}
|
|
||||||
))
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
containers.${machineName} = lib.mkIf (cfg.backend == "container") {
|
|
||||||
autoStart = cfg.autoStart;
|
|
||||||
privateNetwork = lib.mkDefault true;
|
|
||||||
hostBridge = lib.mkIf networked (util.bridgeName cfg.vlan);
|
|
||||||
privateUsers = lib.mkDefault "no";
|
|
||||||
additionalCapabilities = lib.optionals cfg.nesting [ "CAP_NET_ADMIN" ];
|
|
||||||
allowedDevices = lib.optionals cfg.nesting [
|
|
||||||
{
|
|
||||||
node = "/dev/net/tun";
|
|
||||||
modifier = "rwm";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
node = "/dev/fuse";
|
|
||||||
modifier = "rwm";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
bindMounts = userMounts // secretMounts;
|
|
||||||
|
|
||||||
inherit specialArgs;
|
|
||||||
|
|
||||||
config = {
|
|
||||||
imports =
|
|
||||||
[
|
|
||||||
repositoryModules
|
|
||||||
]
|
|
||||||
++ [
|
|
||||||
registryAssembly.module
|
|
||||||
inputs.sops-nix.nixosModules.sops
|
|
||||||
inputs.stylix.nixosModules.stylix
|
|
||||||
{
|
|
||||||
modules.guest.enable = true;
|
|
||||||
}
|
|
||||||
guestNet
|
|
||||||
interior
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
preStart = lib.mkIf (directoryMounts != { }) directoryScript;
|
||||||
|
}
|
||||||
|
(let
|
||||||
|
bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (util.bridgeName cfg.vlan)}.device";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
after = [ bridgeDevice ];
|
||||||
|
wants = [ bridgeDevice ];
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
containers.${name} = {
|
||||||
|
autoStart = true;
|
||||||
|
privateNetwork = lib.mkDefault true;
|
||||||
|
hostBridge = util.bridgeName cfg.vlan;
|
||||||
|
privateUsers = lib.mkDefault "no";
|
||||||
|
bindMounts = lib.mapAttrs (_: mount: {
|
||||||
|
hostPath = if mount.directory != null then mount.directory else mount.file;
|
||||||
|
isReadOnly = mount.readOnly;
|
||||||
|
}) cfg.mounts;
|
||||||
|
|
||||||
|
inherit specialArgs;
|
||||||
|
|
||||||
|
config = {
|
||||||
|
imports = [
|
||||||
|
repositoryModules
|
||||||
|
registryAssembly.module
|
||||||
|
inputs.sops-nix.nixosModules.sops
|
||||||
|
inputs.stylix.nixosModules.stylix
|
||||||
|
{
|
||||||
|
modules.guest.enable = true;
|
||||||
|
}
|
||||||
|
guestNet
|
||||||
|
interior
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit mkGuest;
|
mkGuest =
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
interior ? { },
|
||||||
|
}:
|
||||||
|
{ config, lib, ... }:
|
||||||
|
{
|
||||||
|
options.guests.${name} = mkGuestOptions { inherit config lib name; };
|
||||||
|
config = mkGuestConfig {
|
||||||
|
inherit
|
||||||
|
config
|
||||||
|
interior
|
||||||
|
lib
|
||||||
|
name
|
||||||
|
;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
guests.actualbudget = {
|
guests.actualbudget = {
|
||||||
mounts.${dataDir}.hostPath = cfg.dataPath;
|
mounts.${dataDir}.directory = cfg.dataPath;
|
||||||
limits = {
|
limits = {
|
||||||
memory = lib.mkDefault "2G";
|
memory = lib.mkDefault "2G";
|
||||||
cpu = lib.mkDefault "200%";
|
cpu = lib.mkDefault "200%";
|
||||||
|
|||||||
@@ -106,19 +106,20 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
guests.copyparty = {
|
guests.copyparty = {
|
||||||
secrets = [ cfg.passwordSecret ];
|
|
||||||
mounts =
|
mounts =
|
||||||
{
|
{
|
||||||
${cfg.stateDir} = {
|
${cfg.stateDir} = {
|
||||||
hostPath = cfg.statePath;
|
directory = cfg.statePath;
|
||||||
create = {
|
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
group = "storage";
|
||||||
group = "storage";
|
mode = "0770";
|
||||||
mode = "0770";
|
};
|
||||||
};
|
${"/run/secrets/${cfg.passwordSecret}"} = {
|
||||||
|
file = config.sops.secrets.${cfg.passwordSecret}.path;
|
||||||
|
readOnly = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// lib.mapAttrs' (_urlPath: volume: lib.nameValuePair volume.path { inherit (volume) hostPath; }) cfg.volumes;
|
// lib.mapAttrs' (_urlPath: volume: lib.nameValuePair volume.path { directory = volume.hostPath; }) cfg.volumes;
|
||||||
limits = {
|
limits = {
|
||||||
memory = lib.mkDefault "2G";
|
memory = lib.mkDefault "2G";
|
||||||
cpu = lib.mkDefault "200%";
|
cpu = lib.mkDefault "200%";
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
args@{ flake, ... }:
|
|
||||||
# A sample guest whose interior runs an OCI container on Podman.
|
|
||||||
# The image is pulled at runtime, so the guest builds with no build-time fetch.
|
|
||||||
flake.mkGuest {
|
|
||||||
name = "nesting-sample";
|
|
||||||
interior = {
|
|
||||||
virtualisation.oci-containers.containers.hello = {
|
|
||||||
image = "docker.io/library/hello-world";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} args
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
args@{ flake, ... }:
|
|
||||||
# The tracer-bullet guest: the thinnest complete path from discovery to a
|
|
||||||
# running nested container. Its interior is just the guest-base — the baseline
|
|
||||||
# toolset and SSH access — so it proves the concept without carrying a service.
|
|
||||||
flake.mkGuest { name = "sample"; } args
|
|
||||||
@@ -40,26 +40,6 @@
|
|||||||
modules.network.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
|
modules.network.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
|
||||||
modules.network.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
|
modules.network.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
|
||||||
|
|
||||||
# The walking-skeleton guest, enabled like any module: proves the guest path
|
|
||||||
# end to end through this host's `nix flake check`.
|
|
||||||
# Modest caps keep the skeleton guest from starving the laptop.
|
|
||||||
guests.sample.enable = true;
|
|
||||||
guests.sample.limits = {
|
|
||||||
memory = "1G";
|
|
||||||
cpu = "100%";
|
|
||||||
tasksMax = 512;
|
|
||||||
};
|
|
||||||
|
|
||||||
# The nesting guest, run with `nesting` on: proves an interior OCI container
|
|
||||||
# on Podman builds end to end through this host's `nix flake check`.
|
|
||||||
guests.nesting-sample.enable = true;
|
|
||||||
guests.nesting-sample.nesting = true;
|
|
||||||
guests.nesting-sample.limits = {
|
|
||||||
memory = "1G";
|
|
||||||
cpu = "100%";
|
|
||||||
tasksMax = 512;
|
|
||||||
};
|
|
||||||
|
|
||||||
modules.shell.herdr.enable = true;
|
modules.shell.herdr.enable = true;
|
||||||
modules.agents.tools.gitea-axi.enable = true;
|
modules.agents.tools.gitea-axi.enable = true;
|
||||||
modules.agents.pi.enable = true;
|
modules.agents.pi.enable = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user