Compare commits
10 Commits
d3d3987f86
...
dc74538cfe
| Author | SHA1 | Date | |
|---|---|---|---|
| dc74538cfe | |||
| 28d8eb7ca9 | |||
| 42b1578efa | |||
| 4f105c81c2 | |||
| 0abd3121fb | |||
| d88ee49aaf | |||
| 647b20b934 | |||
| 6f0f586fb6 | |||
| be4a060e94 | |||
| 1ba3bdf8d6 |
@@ -4,13 +4,16 @@ One flake builds every machine I own.
|
|||||||
|
|
||||||
## Repository Conventions
|
## Repository Conventions
|
||||||
|
|
||||||
- Follow `docs/technical-writing.md` for comments, user-facing descriptions, commit prose, pull-request prose, and Markdown documentation.
|
- Use `docs/technical-writing.md` and `docs/dictionary.md` while authoring any file.
|
||||||
|
Apply them to comments, user-facing descriptions, commit prose, pull-request prose, and Markdown documentation.
|
||||||
- Comments posted to the forge under the operator's account must end with `— Pi`.
|
- Comments posted to the forge under the operator's account must end with `— Pi`.
|
||||||
The sign-off identifies the agent as the author instead of the operator.
|
The sign-off identifies the agent as the author instead of the operator.
|
||||||
- Commit messages follow Conventional Commits, defined in `docs/conventional-commits.md`.
|
- Commit messages follow Conventional Commits, defined in `docs/conventional-commits.md`.
|
||||||
Use a scope for the changed module or host, such as `fish`, `nvim`, or `neogaia`.
|
Use a scope for the changed module or host, such as `fish`, `nvim`, or `neogaia`.
|
||||||
- Keep commit messages about the Git history, not the forge workflow.
|
- Keep commit messages about the Git history, not the forge workflow.
|
||||||
Do not reference issue or pull-request numbers unless the commit itself requires a stable external reference.
|
Do not reference issue or pull-request numbers unless the commit itself requires a stable external reference.
|
||||||
|
- During pull-request development, add follow-up commits instead of amending existing commits.
|
||||||
|
Preserve review history until the pull request is ready to merge.
|
||||||
- Use `gitea-axi` for Gitea issues, pull requests, labels, reviews, comments, and milestones.
|
- Use `gitea-axi` for Gitea issues, pull requests, labels, reviews, comments, and milestones.
|
||||||
It resolves this repository from the `origin` remote inside the checkout.
|
It resolves this repository from the `origin` remote inside the checkout.
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ One flake builds every machine I own.
|
|||||||
- Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths.
|
- Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths.
|
||||||
For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths.
|
For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths.
|
||||||
Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts.
|
Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts.
|
||||||
- `self.lib.registry.collect { hosts = self.nixosConfigurations; }` reads only top-level host `config.registry` declarations.
|
- The registry collector reads only top-level host `config.registry` declarations.
|
||||||
Registry declarations inside guest interiors live under `config.containers.<guest>.config.registry`.
|
Registry declarations inside guest interiors live under `config.containers.<guest>.config.registry`.
|
||||||
The collected registry does not include those guest-interior declarations.
|
The collected registry does not include those guest-interior declarations.
|
||||||
Publish guest values from host-side code, or extend the collector to recurse into guests.
|
Publish guest values from host-side code, or extend the collector to recurse into guests.
|
||||||
|
|||||||
@@ -45,11 +45,15 @@ Use the simplest accurate word when a term is not listed.
|
|||||||
| operator | Agent tooling | The human driving the development process. Use when distinguishing the human from the agent acting on their behalf. |
|
| operator | Agent tooling | The human driving the development process. Use when distinguishing the human from the agent acting on their behalf. |
|
||||||
| guest | dotfiles | A systemd-nspawn container managed by this flake. Use for configured containers under `guests/`. |
|
| guest | dotfiles | A systemd-nspawn container managed by this flake. Use for configured containers under `guests/`. |
|
||||||
| host | dotfiles | A machine with a NixOS configuration built by this flake. |
|
| host | dotfiles | A machine with a NixOS configuration built by this flake. |
|
||||||
| module | dotfiles | In this repository, a self-contained configuration under `modules/`, usually scoped to one program, service, or subsystem. Use `Nix module` for the language-level module-system concept. |
|
| module | dotfiles | In this repository, a self-contained configuration under `modules/`. Use for one program, service, or subsystem. |
|
||||||
| registry | dotfiles | A typed configuration namespace whose entries can be collected across hosts and reused by other configuration. |
|
| registry | dotfiles | A typed configuration namespace whose entries can be collected across hosts and reused by other configuration. |
|
||||||
| Conventional Commits | Git | A commit-message format defined in `docs/conventional-commits.md`. |
|
| Conventional Commits | Git | A commit-message format defined in `docs/conventional-commits.md`. |
|
||||||
| forge | Git | A web service that hosts Git repositories, pull requests, issues, and code review comments. Use when the exact provider does not matter. |
|
| forge | Git | A web service that hosts Git repositories, pull requests, issues, and code review comments. Use when the exact provider does not matter. |
|
||||||
| flake | Nix | A Nix project with pinned inputs and declared outputs. Use for the repository-level build unit defined by `flake.nix`. |
|
| flake | Nix | A Nix project with pinned inputs and declared outputs. Use for the repository-level build unit defined by `flake.nix`. |
|
||||||
| Nix module | Nix | A Nix language unit that declares or sets options through the Nix module system. |
|
| assembly file | Nix | A Nix file under `flake/` that constructs flake outputs or arguments passed to NixOS modules. |
|
||||||
|
| assembly function | Nix | A project function in `flake/` that creates modules, systems, or output values. |
|
||||||
|
| library file | Nix | A Nix file that defines reusable project functions instead of a NixOS module or flake output. |
|
||||||
|
| library function | Nix | A pure project function exposed under `util`. |
|
||||||
|
| NixOS module | Nix | A Nix file evaluated by `nixosSystem` for a NixOS configuration. |
|
||||||
| overlay | Nix | A function that modifies a Nix package set by adding or replacing packages. |
|
| overlay | Nix | A function that modifies a Nix package set by adding or replacing packages. |
|
||||||
| workspace | Window management | A named or numbered group of windows in a graphical session. Some desktop environments call this a virtual desktop. |
|
| workspace | Window management | A named or numbered group of windows in a graphical session. Some desktop environments call this a virtual desktop. |
|
||||||
|
|||||||
270
docs/nix.md
Normal file
270
docs/nix.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Nix Conventions
|
||||||
|
|
||||||
|
This document records local Nix style and design conventions.
|
||||||
|
Use these conventions when changing this flake.
|
||||||
|
Read this document with `docs/dictionary.md`.
|
||||||
|
|
||||||
|
## Names
|
||||||
|
|
||||||
|
### Reserve `lib` for `nixpkgs.lib`
|
||||||
|
|
||||||
|
Use `lib` only for the base `nixpkgs.lib` value.
|
||||||
|
Do not use `lib` for project functions.
|
||||||
|
Use `util` for library functions passed through module arguments.
|
||||||
|
Use `flake.mk<Name>` for assembly functions used by flake outputs.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
specialArgs = {
|
||||||
|
lib = util;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
specialArgs = {
|
||||||
|
inherit util;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Then call library functions through `util` inside modules:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
util,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
networking.networkmanager.unmanaged = [ "interface-name:${util.bridgeName 10}" ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Name project function namespaces by role
|
||||||
|
|
||||||
|
Put pure project functions under `util`.
|
||||||
|
Put functions that create systems or output values under `flake`.
|
||||||
|
This keeps data-only functions separate from functions that carry flake context.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
util.mkHosts { root = self + "/hosts"; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
flake.mkHosts { root = self + "/hosts"; }
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Placement
|
||||||
|
|
||||||
|
### Put files in the directory that owns their role
|
||||||
|
|
||||||
|
Choose the file location before writing a Nix file.
|
||||||
|
Put assembly files under `flake/`.
|
||||||
|
Put library files under `lib/`.
|
||||||
|
Put modules under `modules/`.
|
||||||
|
Put host configurations under `hosts/<name>/`.
|
||||||
|
Put host-side guest wiring under `guests/`.
|
||||||
|
Put registry definitions under `registry/`.
|
||||||
|
Keep `flake.nix` focused on inputs and output wiring.
|
||||||
|
Do not add root-level Nix files other than `flake.nix`.
|
||||||
|
|
||||||
|
A module is self-contained configuration under `modules/`.
|
||||||
|
A module configures one program, service, or subsystem.
|
||||||
|
Files under `hosts/`, `guests/`, and `modules/` return NixOS modules.
|
||||||
|
|
||||||
|
### Use `default.nix` for module directories with support files
|
||||||
|
|
||||||
|
Use `default.nix` as the primary module when a module directory owns support files.
|
||||||
|
Keep sibling files for data or code read by that primary module.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```text
|
||||||
|
modules/shell/fish/fish.nix
|
||||||
|
modules/shell/fish/config.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
modules/shell/fish/default.nix
|
||||||
|
modules/shell/fish/config.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use non-module `default.nix` files only for directory assembly
|
||||||
|
|
||||||
|
Outside `modules/`, use `default.nix` to connect files in its directory.
|
||||||
|
Do not define primary assembly functions in `default.nix`.
|
||||||
|
Put primary assembly functions in named files.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```text
|
||||||
|
flake/default.nix # defines mkHost
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
flake/default.nix # imports ./host.nix
|
||||||
|
flake/host.nix # defines mkHosts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keep assembly files focused on one concept
|
||||||
|
|
||||||
|
Give each assembly file under `flake/` one top-level concept.
|
||||||
|
Keep helper functions private to that file when no other file needs them.
|
||||||
|
Export the constructor for the concept when another assembly file needs it.
|
||||||
|
Name that constructor `mk<Concept>`.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inherit collectNixFiles mkHost mkHosts;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inherit mkHosts;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Function Arguments
|
||||||
|
|
||||||
|
### Use closed interfaces for assembly files and library files
|
||||||
|
|
||||||
|
Use exact function argument patterns for assembly files and library files.
|
||||||
|
The caller owns the argument set for these files.
|
||||||
|
The imported file names each accepted attribute.
|
||||||
|
The imported file rejects stale or accidental extra attributes.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs,
|
||||||
|
lib,
|
||||||
|
self,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs,
|
||||||
|
lib,
|
||||||
|
self,
|
||||||
|
}:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pass function arguments explicitly
|
||||||
|
|
||||||
|
Pass only the values an assembly file or library file needs.
|
||||||
|
Bind `lib` to the base `nixpkgs.lib` value at the call site.
|
||||||
|
Use `inherit` only for values that keep the same name.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
import ./flake inputs
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let
|
||||||
|
inherit (nixpkgs) lib;
|
||||||
|
in
|
||||||
|
import ./flake { inherit lib inputs self; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keep NixOS module interfaces open
|
||||||
|
|
||||||
|
Files under `hosts/`, `guests/`, and `modules/` return NixOS modules.
|
||||||
|
`nixosSystem` owns their argument shape.
|
||||||
|
Name each argument the module uses.
|
||||||
|
Keep `...` so the module accepts standard arguments and special arguments.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
registry,
|
||||||
|
}:
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
registry,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
```
|
||||||
|
|
||||||
|
## `let` Bindings
|
||||||
|
|
||||||
|
### Use role-specific binding names
|
||||||
|
|
||||||
|
A `let` binding name must explain the value's role in the expression.
|
||||||
|
Avoid names that describe only the value's type.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let
|
||||||
|
names = lib.attrNames hosts;
|
||||||
|
in
|
||||||
|
lib.genAttrs names mkHost
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let
|
||||||
|
hostNames = lib.attrNames hosts;
|
||||||
|
in
|
||||||
|
lib.genAttrs hostNames mkHost
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standard Library Calls
|
||||||
|
|
||||||
|
### Qualify standard library calls in flake assembly
|
||||||
|
|
||||||
|
Use qualified standard library calls inside assembly files.
|
||||||
|
Use `lib.mapAttrs` instead of inheriting `mapAttrs` from `lib`.
|
||||||
|
This keeps the source of each function visible.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let
|
||||||
|
inherit (lib) mapAttrs;
|
||||||
|
in
|
||||||
|
mapAttrs f attrs
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
lib.mapAttrs f attrs
|
||||||
|
```
|
||||||
|
|
||||||
@@ -76,14 +76,9 @@
|
|||||||
{ self, nixpkgs, ... }@inputs:
|
{ self, nixpkgs, ... }@inputs:
|
||||||
let
|
let
|
||||||
inherit (nixpkgs) lib;
|
inherit (nixpkgs) lib;
|
||||||
my = import ./lib { inherit lib inputs self; };
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Helper functions for discovering and building hosts.
|
inherit (import ./flake { inherit lib inputs self; }) nixosConfigurations;
|
||||||
lib = my;
|
|
||||||
|
|
||||||
# Every host under hosts/ is discovered and built.
|
|
||||||
nixosConfigurations = my.mkHosts (self + "/hosts");
|
|
||||||
|
|
||||||
# A project shell for agent-local resources that should travel with this
|
# A project shell for agent-local resources that should travel with this
|
||||||
# checkout rather than the operator's global profile.
|
# checkout rather than the operator's global profile.
|
||||||
|
|||||||
53
flake/default.nix
Normal file
53
flake/default.nix
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
inputs,
|
||||||
|
self,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
util = import ../lib { inherit lib; };
|
||||||
|
|
||||||
|
registry = import ./registry.nix {
|
||||||
|
inherit lib;
|
||||||
|
hosts = self.nixosConfigurations;
|
||||||
|
root = self + "/registry";
|
||||||
|
};
|
||||||
|
|
||||||
|
modules = import ./module.nix {
|
||||||
|
inherit util;
|
||||||
|
root = self + "/modules";
|
||||||
|
};
|
||||||
|
|
||||||
|
guestModules = import ./guest.nix {
|
||||||
|
inherit
|
||||||
|
inputs
|
||||||
|
lib
|
||||||
|
registry
|
||||||
|
specialArgs
|
||||||
|
util
|
||||||
|
;
|
||||||
|
repositoryModules = modules;
|
||||||
|
root = self + "/guests";
|
||||||
|
};
|
||||||
|
|
||||||
|
specialArgs = {
|
||||||
|
inherit
|
||||||
|
inputs
|
||||||
|
self
|
||||||
|
util
|
||||||
|
;
|
||||||
|
registry = registry.collected;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixosConfigurations = import ./host.nix {
|
||||||
|
inherit
|
||||||
|
inputs
|
||||||
|
lib
|
||||||
|
specialArgs
|
||||||
|
guestModules
|
||||||
|
;
|
||||||
|
registryModule = registry.module;
|
||||||
|
repositoryModules = modules;
|
||||||
|
root = self + "/hosts";
|
||||||
|
};
|
||||||
|
}
|
||||||
279
flake/guest.nix
Normal file
279
flake/guest.nix
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
{
|
||||||
|
inputs,
|
||||||
|
lib,
|
||||||
|
root,
|
||||||
|
specialArgs,
|
||||||
|
repositoryModules,
|
||||||
|
registry,
|
||||||
|
util,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
mkGuest =
|
||||||
|
name:
|
||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.guests.${name};
|
||||||
|
|
||||||
|
directoryMounts = lib.filterAttrs (_: mount: mount.directory != null) cfg.mounts;
|
||||||
|
|
||||||
|
directoryScript = lib.concatStringsSep "\n" (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
_: mount:
|
||||||
|
''
|
||||||
|
${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}
|
||||||
|
''
|
||||||
|
) directoryMounts
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.guests.${name} = {
|
||||||
|
enable = lib.mkEnableOption "the ${name} guest, run in its own nested container";
|
||||||
|
vlan = lib.mkOption {
|
||||||
|
type = lib.types.ints.between 1 4094;
|
||||||
|
example = 10;
|
||||||
|
description = ''
|
||||||
|
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
|
||||||
|
`modules.network.vlans.ids`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
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";
|
||||||
|
|
||||||
|
route = lib.mkOption {
|
||||||
|
type = registry.types.route;
|
||||||
|
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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
interior = lib.mkOption {
|
||||||
|
type = lib.types.deferredModule;
|
||||||
|
default = { };
|
||||||
|
description = "NixOS module imported into the guest interior.";
|
||||||
|
};
|
||||||
|
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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions =
|
||||||
|
let
|
||||||
|
hasDirectorySettings = mount: mount.owner != null || mount.group != null || mount.mode != null;
|
||||||
|
hasCompleteDirectorySettings = mount: mount.owner != null && mount.group != null && mount.mode != null;
|
||||||
|
hasOneSource = mount: (mount.directory != null) != (mount.file != null);
|
||||||
|
mountsWithInvalidSource = lib.attrNames (lib.filterAttrs (_: mount: !hasOneSource mount) cfg.mounts);
|
||||||
|
fileMountsWithHostSettings = lib.attrNames (
|
||||||
|
lib.filterAttrs (_: mount: mount.file != null && hasDirectorySettings mount) cfg.mounts
|
||||||
|
);
|
||||||
|
directoryMountsWithPartialHostSettings = lib.attrNames (
|
||||||
|
lib.filterAttrs (_: mount: mount.directory != null && hasDirectorySettings mount && !hasCompleteDirectorySettings mount) cfg.mounts
|
||||||
|
);
|
||||||
|
in
|
||||||
|
[
|
||||||
|
{
|
||||||
|
assertion = mountsWithInvalidSource == [ ];
|
||||||
|
message = ''
|
||||||
|
guests.${name} mounts ${lib.concatStringsSep ", " mountsWithInvalidSource} must set exactly one of `directory` or `file`.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = fileMountsWithHostSettings == [ ];
|
||||||
|
message = ''
|
||||||
|
guests.${name} file mounts ${lib.concatStringsSep ", " fileMountsWithHostSettings} must not set `owner`, `group`, or `mode`.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = directoryMountsWithPartialHostSettings == [ ];
|
||||||
|
message = ''
|
||||||
|
guests.${name} directory mounts ${lib.concatStringsSep ", " directoryMountsWithPartialHostSettings} must set `owner`, `group`, and `mode` together.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = lib.elem cfg.vlan config.modules.network.vlans.ids;
|
||||||
|
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.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
registry.address.${config.networking.hostName}.${name} =
|
||||||
|
lib.mkIf (cfg.address != null) cfg.address;
|
||||||
|
|
||||||
|
registry.route.${config.networking.hostName}.${name} =
|
||||||
|
lib.mkIf cfg.reverseProxy.enable cfg.reverseProxy.route;
|
||||||
|
|
||||||
|
systemd.services."container@${name}" =
|
||||||
|
let
|
||||||
|
bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (util.bridgeName cfg.vlan)}.device";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
after = [ bridgeDevice ];
|
||||||
|
wants = [ bridgeDevice ];
|
||||||
|
|
||||||
|
serviceConfig = lib.filterAttrs (_: v: v != null) {
|
||||||
|
MemoryMax = cfg.limits.memory;
|
||||||
|
CPUQuota = cfg.limits.cpu;
|
||||||
|
TasksMax = cfg.limits.tasksMax;
|
||||||
|
};
|
||||||
|
preStart = lib.mkIf (directoryMounts != { }) directoryScript;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
inputs.sops-nix.nixosModules.sops
|
||||||
|
inputs.stylix.nixosModules.stylix
|
||||||
|
|
||||||
|
repositoryModules
|
||||||
|
registry.module
|
||||||
|
cfg.interior
|
||||||
|
|
||||||
|
{
|
||||||
|
modules.guest.enable = true;
|
||||||
|
|
||||||
|
networking.useNetworkd = true;
|
||||||
|
networking.useHostResolvConf = false;
|
||||||
|
|
||||||
|
systemd.network.networks."20-eth0" = {
|
||||||
|
matchConfig.Name = "eth0";
|
||||||
|
linkConfig.MACAddress = cfg.mac;
|
||||||
|
networkConfig.DHCP = "yes";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
let
|
||||||
|
files = util.collectNixFiles root;
|
||||||
|
guestFiles = lib.filterAttrs (file: type: type == "regular" && lib.hasSuffix ".nix" file) (builtins.readDir root);
|
||||||
|
names = map (file: lib.removeSuffix ".nix" file) (lib.attrNames guestFiles);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = files ++ map mkGuest names;
|
||||||
|
}
|
||||||
45
flake/host.nix
Normal file
45
flake/host.nix
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
inputs,
|
||||||
|
lib,
|
||||||
|
root,
|
||||||
|
specialArgs,
|
||||||
|
repositoryModules,
|
||||||
|
|
||||||
|
guestModules,
|
||||||
|
registryModule,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
mkHost =
|
||||||
|
{
|
||||||
|
root,
|
||||||
|
name,
|
||||||
|
}:
|
||||||
|
lib.nixosSystem {
|
||||||
|
inherit specialArgs;
|
||||||
|
modules = [
|
||||||
|
inputs.chaotic.nixosModules.default
|
||||||
|
inputs.disko.nixosModules.disko
|
||||||
|
inputs.sops-nix.nixosModules.sops
|
||||||
|
inputs.stylix.nixosModules.stylix
|
||||||
|
|
||||||
|
repositoryModules
|
||||||
|
guestModules
|
||||||
|
|
||||||
|
# TODO: refactor
|
||||||
|
registryModule (root + "/${name}")
|
||||||
|
|
||||||
|
{
|
||||||
|
networking.hostName = name;
|
||||||
|
modules.host.enable = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
let
|
||||||
|
directories = lib.filterAttrs (_name: type: type == "directory") (builtins.readDir root);
|
||||||
|
names = lib.attrNames directories;
|
||||||
|
in
|
||||||
|
lib.genAttrs names (
|
||||||
|
name:
|
||||||
|
mkHost { inherit root name; }
|
||||||
|
)
|
||||||
7
flake/module.nix
Normal file
7
flake/module.nix
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
root,
|
||||||
|
util,
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports = util.collectNixFiles root;
|
||||||
|
}
|
||||||
@@ -1,35 +1,16 @@
|
|||||||
{ lib, self }:
|
{ hosts, lib, root }:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
checkType = lib.types.submodule {
|
||||||
all
|
|
||||||
concatLists
|
|
||||||
concatMapStringsSep
|
|
||||||
filter
|
|
||||||
filterAttrs
|
|
||||||
foldl'
|
|
||||||
hasAttr
|
|
||||||
hasSuffix
|
|
||||||
mapAttrs
|
|
||||||
mapAttrs'
|
|
||||||
mapAttrsToList
|
|
||||||
mkOption
|
|
||||||
nameValuePair
|
|
||||||
removeSuffix
|
|
||||||
types
|
|
||||||
unique
|
|
||||||
;
|
|
||||||
|
|
||||||
checkType = types.submodule {
|
|
||||||
options = {
|
options = {
|
||||||
assertion = mkOption {
|
assertion = lib.mkOption {
|
||||||
type = types.functionTo types.bool;
|
type = lib.types.functionTo lib.types.bool;
|
||||||
description = ''
|
description = ''
|
||||||
Predicate run against this registry's flattened collected entries.
|
Predicate run against this registry's flattened collected entries.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
message = mkOption {
|
message = lib.mkOption {
|
||||||
type = types.str;
|
type = lib.types.str;
|
||||||
description = ''
|
description = ''
|
||||||
Error message shown when this registry validation check fails.
|
Error message shown when this registry validation check fails.
|
||||||
'';
|
'';
|
||||||
@@ -38,7 +19,7 @@ let
|
|||||||
};
|
};
|
||||||
|
|
||||||
predicates = {
|
predicates = {
|
||||||
uniqueValue = value: entries: builtins.length (unique (map value entries)) == builtins.length entries;
|
uniqueValue = value: entries: builtins.length (lib.unique (map value entries)) == builtins.length entries;
|
||||||
};
|
};
|
||||||
|
|
||||||
registryLib = lib // {
|
registryLib = lib // {
|
||||||
@@ -49,23 +30,23 @@ let
|
|||||||
|
|
||||||
registryDefinitionModule = {
|
registryDefinitionModule = {
|
||||||
options = {
|
options = {
|
||||||
type = mkOption {
|
type = lib.mkOption {
|
||||||
type = types.raw;
|
type = lib.types.raw;
|
||||||
description = ''
|
description = ''
|
||||||
NixOS option type for one registry entry.
|
NixOS option type for one registry entry.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
replicate = mkOption {
|
replicate = lib.mkOption {
|
||||||
type = types.bool;
|
type = lib.types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description = ''
|
description = ''
|
||||||
Whether collected entries are merged and published under every host key.
|
Whether collected entries are merged and published under every host key.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
checks = mkOption {
|
checks = lib.mkOption {
|
||||||
type = types.listOf checkType;
|
type = lib.types.listOf checkType;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
Validation checks for this registry's flattened collected entries.
|
Validation checks for this registry's flattened collected entries.
|
||||||
@@ -74,10 +55,10 @@ let
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
registryName = fileName: removeSuffix ".nix" fileName;
|
registryName = fileName: lib.removeSuffix ".nix" fileName;
|
||||||
|
|
||||||
registryOption = registry: mkOption {
|
registryOption = registry: lib.mkOption {
|
||||||
type = types.attrsOf (types.attrsOf registry.type);
|
type = lib.types.attrsOf (lib.types.attrsOf registry.type);
|
||||||
default = { };
|
default = { };
|
||||||
description = ''
|
description = ''
|
||||||
Lazily collected fleet-visible registry entries, nested by host and entry name.
|
Lazily collected fleet-visible registry entries, nested by host and entry name.
|
||||||
@@ -98,19 +79,19 @@ let
|
|||||||
inherit (evaluated.config) type replicate checks;
|
inherit (evaluated.config) type replicate checks;
|
||||||
};
|
};
|
||||||
|
|
||||||
definitions = mapAttrs' (
|
definitions = lib.mapAttrs' (
|
||||||
name: type:
|
name: type:
|
||||||
let
|
let
|
||||||
path = self + "/registry/${name}";
|
path = root + "/${name}";
|
||||||
in
|
in
|
||||||
nameValuePair (registryName name) (mkRegistry path)
|
lib.nameValuePair (registryName name) (mkRegistry path)
|
||||||
) (filterAttrs (name: type: type == "regular" && hasSuffix ".nix" name) (builtins.readDir (self + "/registry")));
|
) (lib.filterAttrs (name: type: type == "regular" && lib.hasSuffix ".nix" name) (builtins.readDir root));
|
||||||
|
|
||||||
flattenRegistryEntries = registryName: data:
|
flattenRegistryEntries = registryName: data:
|
||||||
concatLists (
|
lib.concatLists (
|
||||||
mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
host: entries:
|
host: entries:
|
||||||
mapAttrsToList (entry: value: {
|
lib.mapAttrsToList (entry: value: {
|
||||||
path = "registry.${registryName}.${host}.${entry}";
|
path = "registry.${registryName}.${host}.${entry}";
|
||||||
inherit host entry value;
|
inherit host entry value;
|
||||||
}) entries
|
}) entries
|
||||||
@@ -118,17 +99,17 @@ let
|
|||||||
);
|
);
|
||||||
|
|
||||||
rawDeclarations = registryName: hosts:
|
rawDeclarations = registryName: hosts:
|
||||||
mapAttrs (_hostName: host: host.config.registry.${registryName} or { }) hosts;
|
lib.mapAttrs (_hostName: host: host.config.registry.${registryName} or { }) hosts;
|
||||||
|
|
||||||
replicatedEntries = registryName: raw:
|
replicatedEntries = registryName: raw:
|
||||||
let
|
let
|
||||||
entries = concatLists (
|
entries = lib.concatLists (
|
||||||
concatLists (
|
lib.concatLists (
|
||||||
mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
_evalHost: hostDeclarations:
|
_evalHost: hostDeclarations:
|
||||||
mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
hostName: declarations:
|
hostName: declarations:
|
||||||
mapAttrsToList (entryName: value: {
|
lib.mapAttrsToList (entryName: value: {
|
||||||
path = "registry.${registryName}.${hostName}.${entryName}";
|
path = "registry.${registryName}.${hostName}.${entryName}";
|
||||||
inherit entryName value;
|
inherit entryName value;
|
||||||
}) declarations
|
}) declarations
|
||||||
@@ -137,9 +118,9 @@ let
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
in
|
in
|
||||||
foldl' (
|
lib.foldl' (
|
||||||
acc: entry:
|
acc: entry:
|
||||||
if hasAttr entry.entryName acc then
|
if lib.hasAttr entry.entryName acc then
|
||||||
if acc.${entry.entryName}.value == entry.value then
|
if acc.${entry.entryName}.value == entry.value then
|
||||||
acc
|
acc
|
||||||
else
|
else
|
||||||
@@ -152,13 +133,13 @@ let
|
|||||||
|
|
||||||
replicatedData = registryName: hosts: raw:
|
replicatedData = registryName: hosts: raw:
|
||||||
let
|
let
|
||||||
entries = mapAttrs (_entryName: entry: entry.value) (replicatedEntries registryName raw);
|
entries = lib.mapAttrs (_entryName: entry: entry.value) (replicatedEntries registryName raw);
|
||||||
in
|
in
|
||||||
mapAttrs (_hostName: _host: entries) hosts;
|
lib.mapAttrs (_hostName: _host: entries) hosts;
|
||||||
|
|
||||||
validationAssertions = data:
|
validationAssertions = data:
|
||||||
concatLists (
|
lib.concatLists (
|
||||||
mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
registryName: registry:
|
registryName: registry:
|
||||||
let
|
let
|
||||||
entries = flattenRegistryEntries registryName data.${registryName};
|
entries = flattenRegistryEntries registryName data.${registryName};
|
||||||
@@ -173,28 +154,28 @@ let
|
|||||||
assertValid = data:
|
assertValid = data:
|
||||||
let
|
let
|
||||||
assertions = validationAssertions data;
|
assertions = validationAssertions data;
|
||||||
failures = filter (assertion: !assertion.assertion) assertions;
|
failures = lib.filter (assertion: !assertion.assertion) assertions;
|
||||||
in
|
in
|
||||||
assert lib.assertMsg (all (assertion: assertion.assertion) assertions) (
|
assert lib.assertMsg (lib.all (assertion: assertion.assertion) assertions) (
|
||||||
concatMapStringsSep "\n" (assertion: assertion.message) failures
|
lib.concatMapStringsSep "\n" (assertion: assertion.message) failures
|
||||||
);
|
);
|
||||||
data;
|
data;
|
||||||
|
|
||||||
join = registry: host: entry:
|
join = registry: host: entry:
|
||||||
filterAttrs (_name: value: value != null) (
|
lib.filterAttrs (_name: value: value != null) (
|
||||||
mapAttrs (registryName: _registry: (registry.${registryName}.${host} or { }).${entry} or null) definitions
|
lib.mapAttrs (registryName: _registry: (registry.${registryName}.${host} or { }).${entry} or null) definitions
|
||||||
);
|
);
|
||||||
|
|
||||||
collectData =
|
collectData =
|
||||||
{ hosts }:
|
{ hosts }:
|
||||||
let
|
let
|
||||||
raw = mapAttrs (registryName: _registry: rawDeclarations registryName hosts) definitions;
|
raw = lib.mapAttrs (registryName: _registry: rawDeclarations registryName hosts) definitions;
|
||||||
data = mapAttrs (
|
data = lib.mapAttrs (
|
||||||
registryName: registry:
|
registryName: registry:
|
||||||
if registry.replicate or false then
|
if registry.replicate or false then
|
||||||
replicatedData registryName hosts raw.${registryName}
|
replicatedData registryName hosts raw.${registryName}
|
||||||
else
|
else
|
||||||
mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts
|
lib.mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts
|
||||||
) definitions;
|
) definitions;
|
||||||
in
|
in
|
||||||
assertValid data;
|
assertValid data;
|
||||||
@@ -206,11 +187,11 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit definitions predicates;
|
types = lib.mapAttrs (_name: definition: definition.type) definitions;
|
||||||
|
|
||||||
module = {
|
module = {
|
||||||
options.registry = mapAttrs (_name: registry: registryOption registry) definitions;
|
options.registry = lib.mapAttrs (_name: registry: registryOption registry) definitions;
|
||||||
};
|
};
|
||||||
|
|
||||||
collect = args: withOperations (collectData args);
|
collected = withOperations (collectData { inherit hosts; });
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
my,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -9,18 +8,6 @@ let
|
|||||||
dataDir = "/data";
|
dataDir = "/data";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
|
||||||
(my.guest {
|
|
||||||
name = "actualbudget";
|
|
||||||
interior = {
|
|
||||||
modules.services.actualbudget = {
|
|
||||||
enable = true;
|
|
||||||
inherit dataDir;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
options.guests.actualbudget = {
|
options.guests.actualbudget = {
|
||||||
dataPath = lib.mkOption {
|
dataPath = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
@@ -39,7 +26,13 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
guests.actualbudget = {
|
guests.actualbudget = {
|
||||||
mounts.${dataDir}.hostPath = cfg.dataPath;
|
interior = {
|
||||||
|
modules.services.actualbudget = {
|
||||||
|
enable = true;
|
||||||
|
inherit dataDir;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
mounts.${dataDir}.directory = cfg.dataPath;
|
||||||
limits = {
|
limits = {
|
||||||
memory = lib.mkDefault "2G";
|
memory = lib.mkDefault "2G";
|
||||||
cpu = lib.mkDefault "200%";
|
cpu = lib.mkDefault "200%";
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
my,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -34,26 +33,6 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
|
||||||
(my.guest {
|
|
||||||
name = "copyparty";
|
|
||||||
interior = {
|
|
||||||
modules.services.copyparty = {
|
|
||||||
enable = true;
|
|
||||||
inherit (cfg) accountName stateDir;
|
|
||||||
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
|
||||||
reverseProxy = {
|
|
||||||
enable = cfg.reverseProxy.enable;
|
|
||||||
inherit (cfg.reverseProxy) trustedSources;
|
|
||||||
};
|
|
||||||
volumes = lib.mapAttrs (_urlPath: volume: {
|
|
||||||
inherit (volume) path access flags;
|
|
||||||
}) cfg.volumes;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
options.guests.copyparty = {
|
options.guests.copyparty = {
|
||||||
accountName = lib.mkOption {
|
accountName = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
@@ -106,19 +85,33 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
guests.copyparty = {
|
guests.copyparty = {
|
||||||
secrets = [ cfg.passwordSecret ];
|
interior.modules.services.copyparty = {
|
||||||
|
enable = true;
|
||||||
|
inherit (cfg) accountName stateDir;
|
||||||
|
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||||
|
reverseProxy = {
|
||||||
|
enable = cfg.reverseProxy.enable;
|
||||||
|
inherit (cfg.reverseProxy) trustedSources;
|
||||||
|
};
|
||||||
|
volumes = lib.mapAttrs (_urlPath: volume: {
|
||||||
|
inherit (volume) path access flags;
|
||||||
|
}) cfg.volumes;
|
||||||
|
};
|
||||||
|
|
||||||
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@{ my, ... }:
|
|
||||||
# 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.
|
|
||||||
my.guest {
|
|
||||||
name = "nesting-sample";
|
|
||||||
interior = {
|
|
||||||
virtualisation.oci-containers.containers.hello = {
|
|
||||||
image = "docker.io/library/hello-world";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} args
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
my,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
giteaSshBackend = "10.23.20.45:2022";
|
giteaSshBackend = "10.23.20.45:2022";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
config = lib.mkIf config.guests.reverse-proxy.enable {
|
||||||
(my.guest {
|
guests.reverse-proxy = {
|
||||||
name = "reverse-proxy";
|
|
||||||
interior = {
|
interior = {
|
||||||
modules.services.reverse-proxy.enable = true;
|
modules.services.reverse-proxy.enable = true;
|
||||||
|
|
||||||
@@ -66,11 +64,7 @@ in
|
|||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
config = lib.mkIf config.guests.reverse-proxy.enable {
|
|
||||||
guests.reverse-proxy = {
|
|
||||||
limits = {
|
limits = {
|
||||||
memory = lib.mkDefault "1G";
|
memory = lib.mkDefault "1G";
|
||||||
cpu = lib.mkDefault "100%";
|
cpu = lib.mkDefault "100%";
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
args@{ my, ... }:
|
|
||||||
# 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.
|
|
||||||
my.guest { 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;
|
||||||
|
|||||||
101
lib/default.nix
101
lib/default.nix
@@ -1,100 +1,9 @@
|
|||||||
{
|
{ lib }:
|
||||||
lib,
|
|
||||||
inputs,
|
|
||||||
self,
|
|
||||||
}:
|
|
||||||
let
|
let
|
||||||
inherit (lib)
|
files = import ./files.nix { inherit lib; };
|
||||||
attrNames
|
network = import ./network.nix { inherit lib; };
|
||||||
filterAttrs
|
|
||||||
genAttrs
|
|
||||||
flatten
|
|
||||||
hasSuffix
|
|
||||||
mapAttrsToList
|
|
||||||
;
|
|
||||||
|
|
||||||
registry = import ./registry.nix { inherit lib self; };
|
|
||||||
|
|
||||||
collectNixFiles =
|
|
||||||
dir:
|
|
||||||
flatten (
|
|
||||||
mapAttrsToList (
|
|
||||||
name: type:
|
|
||||||
let
|
|
||||||
path = dir + "/${name}";
|
|
||||||
in
|
|
||||||
if type == "directory" then
|
|
||||||
collectNixFiles path
|
|
||||||
else if type == "regular" && hasSuffix ".nix" name then
|
|
||||||
[ path ]
|
|
||||||
else
|
|
||||||
[ ]
|
|
||||||
) (builtins.readDir dir)
|
|
||||||
);
|
|
||||||
|
|
||||||
moduleFiles = collectNixFiles (self + "/modules");
|
|
||||||
guestFiles = collectNixFiles (self + "/guests");
|
|
||||||
|
|
||||||
specialArgs = {
|
|
||||||
inherit inputs self;
|
|
||||||
my = self.lib;
|
|
||||||
registry = self.lib.registry.collect { hosts = self.nixosConfigurations; };
|
|
||||||
};
|
|
||||||
|
|
||||||
bridgeName = id: "br-vlan${toString id}";
|
|
||||||
|
|
||||||
guests = import ./guests.nix {
|
|
||||||
inherit
|
|
||||||
inputs
|
|
||||||
lib
|
|
||||||
self
|
|
||||||
specialArgs
|
|
||||||
bridgeName
|
|
||||||
moduleFiles
|
|
||||||
registry
|
|
||||||
;
|
|
||||||
};
|
|
||||||
|
|
||||||
mkHost =
|
|
||||||
{
|
|
||||||
hostName,
|
|
||||||
system ? "x86_64-linux",
|
|
||||||
}:
|
|
||||||
inputs.nixpkgs.lib.nixosSystem {
|
|
||||||
inherit system specialArgs;
|
|
||||||
modules =
|
|
||||||
moduleFiles
|
|
||||||
++ guestFiles
|
|
||||||
++ [
|
|
||||||
inputs.chaotic.nixosModules.default
|
|
||||||
inputs.disko.nixosModules.disko
|
|
||||||
inputs.sops-nix.nixosModules.sops
|
|
||||||
inputs.stylix.nixosModules.stylix
|
|
||||||
registry.module
|
|
||||||
(self + "/hosts/${hostName}")
|
|
||||||
{
|
|
||||||
networking.hostName = hostName;
|
|
||||||
modules.host.enable = true;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
mkHosts =
|
|
||||||
hostsDir:
|
|
||||||
let
|
|
||||||
hostNames = attrNames (filterAttrs (_name: type: type == "directory") (builtins.readDir hostsDir));
|
|
||||||
in
|
|
||||||
genAttrs hostNames (hostName: mkHost { inherit hostName; });
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit
|
inherit (files) collectNixFiles;
|
||||||
collectNixFiles
|
inherit (network) bridgeName deriveMac;
|
||||||
mkHost
|
|
||||||
mkHosts
|
|
||||||
bridgeName
|
|
||||||
registry
|
|
||||||
;
|
|
||||||
|
|
||||||
inherit (guests) deriveMac guest;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
22
lib/files.nix
Normal file
22
lib/files.nix
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{ lib }:
|
||||||
|
let
|
||||||
|
collectNixFiles =
|
||||||
|
dir:
|
||||||
|
lib.flatten (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
name: type:
|
||||||
|
let
|
||||||
|
path = dir + "/${name}";
|
||||||
|
in
|
||||||
|
if type == "directory" then
|
||||||
|
collectNixFiles path
|
||||||
|
else if type == "regular" && lib.hasSuffix ".nix" name then
|
||||||
|
[ path ]
|
||||||
|
else
|
||||||
|
[ ]
|
||||||
|
) (builtins.readDir dir)
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit collectNixFiles;
|
||||||
|
}
|
||||||
370
lib/guests.nix
370
lib/guests.nix
@@ -1,370 +0,0 @@
|
|||||||
{
|
|
||||||
inputs,
|
|
||||||
lib,
|
|
||||||
self,
|
|
||||||
specialArgs,
|
|
||||||
bridgeName,
|
|
||||||
moduleFiles,
|
|
||||||
registry,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
inherit (lib) escapeShellArg mapAttrsToList;
|
|
||||||
|
|
||||||
deriveMac =
|
|
||||||
name:
|
|
||||||
let
|
|
||||||
hash = builtins.hashString "sha256" name;
|
|
||||||
octet = i: builtins.substring (i * 2) 2 hash;
|
|
||||||
in
|
|
||||||
lib.concatStringsSep ":" ([ "02" ] ++ map octet [ 0 1 2 3 4 ]);
|
|
||||||
|
|
||||||
guest =
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
interior ? { },
|
|
||||||
}:
|
|
||||||
{ config, lib, ... }:
|
|
||||||
let
|
|
||||||
optionPath = [ "guests" ] ++ lib.splitString "." name;
|
|
||||||
cfg = lib.getAttrFromPath optionPath config;
|
|
||||||
machineName = lib.replaceStrings [ "." ] [ "-" ] name;
|
|
||||||
|
|
||||||
networked = cfg.vlan != null;
|
|
||||||
|
|
||||||
userMounts = lib.mapAttrs (_guestPath: m: {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
createdMounts = lib.filterAttrs (_guestPath: m: m.create != null) cfg.mounts;
|
|
||||||
mountCreateScript = lib.concatStringsSep "\n" (
|
|
||||||
mapAttrsToList (
|
|
||||||
_guestPath: m:
|
|
||||||
let
|
|
||||||
create = m.create;
|
|
||||||
in
|
|
||||||
''
|
|
||||||
${config.system.path}/bin/install -d -o ${escapeShellArg (toString create.owner)} -g ${escapeShellArg create.group} -m ${escapeShellArg create.mode} ${escapeShellArg m.hostPath}
|
|
||||||
''
|
|
||||||
) createdMounts
|
|
||||||
);
|
|
||||||
|
|
||||||
guestNet =
|
|
||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
config = lib.mkIf networked {
|
|
||||||
networking.useNetworkd = true;
|
|
||||||
networking.useHostResolvConf = false;
|
|
||||||
|
|
||||||
systemd.network.networks."20-eth0" = {
|
|
||||||
matchConfig.Name = "eth0";
|
|
||||||
linkConfig.MACAddress = cfg.mac;
|
|
||||||
networkConfig.DHCP = "yes";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options = lib.setAttrByPath optionPath {
|
|
||||||
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 = 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 {
|
|
||||||
type = self.lib.registry.definitions.route.type;
|
|
||||||
description = "Route registry entry published for this guest.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
mounts = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf (
|
|
||||||
lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
hostPath = lib.mkOption {
|
|
||||||
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 = { };
|
|
||||||
example = lib.literalExpression ''
|
|
||||||
{
|
|
||||||
"/data/media" = { hostPath = "/srv/media"; };
|
|
||||||
"/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 == [ ];
|
|
||||||
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.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = cfg.backend == "container";
|
|
||||||
message = ''
|
|
||||||
guests.${name}.backend = "${cfg.backend}" is not implemented. Only the "container" backend is built; "microvm" is reserved for future work.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = !networked || lib.elem cfg.vlan config.modules.network.vlans.ids;
|
|
||||||
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.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services."container@${machineName}" = lib.mkIf (cfg.backend == "container") (
|
|
||||||
lib.mkMerge [
|
|
||||||
{
|
|
||||||
serviceConfig = limitConfig;
|
|
||||||
preStart = lib.mkIf (createdMounts != { }) mountCreateScript;
|
|
||||||
}
|
|
||||||
(lib.mkIf networked (
|
|
||||||
let
|
|
||||||
bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (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 (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 =
|
|
||||||
moduleFiles
|
|
||||||
++ [
|
|
||||||
registry.module
|
|
||||||
inputs.sops-nix.nixosModules.sops
|
|
||||||
inputs.stylix.nixosModules.stylix
|
|
||||||
{
|
|
||||||
modules.guest.enable = true;
|
|
||||||
}
|
|
||||||
guestNet
|
|
||||||
interior
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit deriveMac guest;
|
|
||||||
}
|
|
||||||
12
lib/network.nix
Normal file
12
lib/network.nix
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{ lib }:
|
||||||
|
{
|
||||||
|
bridgeName = id: "br-vlan${toString id}";
|
||||||
|
|
||||||
|
deriveMac =
|
||||||
|
name:
|
||||||
|
let
|
||||||
|
hash = builtins.hashString "sha256" name;
|
||||||
|
octet = i: builtins.substring (i * 2) 2 hash;
|
||||||
|
in
|
||||||
|
lib.concatStringsSep ":" ([ "02" ] ++ map octet [ 0 1 2 3 4 ]);
|
||||||
|
}
|
||||||
@@ -64,7 +64,6 @@ in
|
|||||||
useUserPackages = true;
|
useUserPackages = true;
|
||||||
extraSpecialArgs = {
|
extraSpecialArgs = {
|
||||||
inherit inputs;
|
inherit inputs;
|
||||||
my = inputs.self.lib;
|
|
||||||
};
|
};
|
||||||
users.${user.name} = {
|
users.${user.name} = {
|
||||||
home.username = user.name;
|
home.username = user.name;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
my,
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vlans;
|
cfg = config.modules.network.vlans;
|
||||||
|
|
||||||
inherit (my) bridgeName;
|
inherit (util) bridgeName;
|
||||||
|
|
||||||
vlanName = id: "${cfg.trunk}.${toString id}";
|
vlanName = id: "${cfg.trunk}.${toString id}";
|
||||||
in
|
in
|
||||||
|
|||||||
Reference in New Issue
Block a user