fix(nixos): address Nix review findings #183

Closed
alexion wants to merge 1 commits from fix/nix-review-findings into main
13 changed files with 215 additions and 94 deletions
Showing only changes of commit 66efae302f - Show all commits

View File

@@ -12,43 +12,22 @@ in
interior = {
modules.services.reverse-proxy.enable = true;
services.nginx.virtualHosts = {
"games.alexion.dev" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
"git.alexion.dev" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
"photos.alexion.dev" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
"plex.alexion.dev" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
"servers.alexion.dev" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
"todo.alexion.dev" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
"wings.alexion.dev" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
};
services.nginx.virtualHosts =
lib.genAttrs
[
"games.alexion.dev"
"git.alexion.dev"
"photos.alexion.dev"
"plex.alexion.dev"
"servers.alexion.dev"
"todo.alexion.dev"
"wings.alexion.dev"
]
(_domain: {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
});
networking.firewall.allowedTCPPorts = [ 2022 ];

View File

@@ -60,7 +60,6 @@
serverName = "Tracion";
autoStart = false;
adminPasswordSecret = "services/game-servers/admin/password";
publicIP = config.guests.palworld.address;
reverseProxy = {
enable = true;
route = {

View File

@@ -1,6 +1,5 @@
{
config,
lib,
...
}:
# zeus — Ryzen desktop with AMD graphics.
@@ -8,9 +7,9 @@
# `fileSystems` are derived from it, none declared here.
{
imports = [
./hardware-configuration.nix
./disk.nix
]
++ lib.optional (builtins.pathExists ./hardware-configuration.nix) ./hardware-configuration.nix;
];
nixpkgs.hostPlatform = "x86_64-linux";

View File

@@ -62,18 +62,6 @@
description = "Start the Palworld server after the guest reaches network readiness.";
};
rcon = {
enable = lib.mkEnableOption "Palworld RCON" // {
default = true;
};
port = lib.mkOption {
type = lib.types.port;
default = 25575;
description = "TCP port for Palworld RCON.";
};
};
passwordFile = lib.mkOption {
type = lib.types.path;
description = "Runtime file containing the Palworld server password.";
@@ -89,65 +77,120 @@
let
service = config.modules.services.games.palworld;
savedDir = "${service.installDir}/Pal/Saved";
defaultSettingsFile = "${service.installDir}/DefaultPalWorldSettings.ini";
settingsFile = "${savedDir}/Config/LinuxServer/PalWorldSettings.ini";
renderSettingsScript = pkgs.writeText "render-palworld-settings.py" ''
import json
import os
from pathlib import Path
import re
import sys
import tempfile
default_path, settings_path, server_name, password_path, admin_password_path = sys.argv[1:]
default_path = Path(default_path)
settings_path = Path(settings_path)
source_path = settings_path if settings_path.exists() else default_path
text = source_path.read_text(encoding="utf-8")
def read_secret(path):
return Path(path).read_text(encoding="utf-8").rstrip("\r\n")
def set_string_setting(text, name, value):
assignment = f"{name}={json.dumps(value, ensure_ascii=False)}"
pattern = re.compile(rf'(?<![A-Za-z0-9_]){re.escape(name)}="(?:[^"\\]|\\.)*"')
text, replacements = pattern.subn(lambda _match: assignment, text, count=1)
if replacements:
return text
marker = "OptionSettings=("
if marker not in text:
raise ValueError(f"{source_path} has no OptionSettings declaration")
return text.replace(marker, f"{marker}{assignment},", 1)
settings = {
"ServerName": server_name,
"ServerPassword": read_secret(password_path),
"AdminPassword": read_secret(admin_password_path),
}
for name, value in settings.items():
text = set_string_setting(text, name, value)
settings_path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(
prefix=f".{settings_path.name}.",
dir=settings_path.parent,
)
temporary_path = Path(temporary_name)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as settings_file:
settings_file.write(text)
os.replace(temporary_path, settings_path)
finally:
temporary_path.unlink(missing_ok=True)
'';
startScript = pkgs.writeShellScript "palworld-start" ''
set -eu
${pkgs.coreutils}/bin/install -d \
${lib.getExe' pkgs.coreutils "install"} -d \
-m 0770 \
-- \
${lib.escapeShellArg service.stateDir} \
${lib.escapeShellArg (builtins.dirOf savedDir)}
if [ -L ${lib.escapeShellArg savedDir} ]; then
current_target="$(${pkgs.coreutils}/bin/readlink ${lib.escapeShellArg savedDir})"
current_target="$(${lib.getExe' pkgs.coreutils "readlink"} -- ${lib.escapeShellArg savedDir})"
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
${pkgs.coreutils}/bin/rm ${lib.escapeShellArg savedDir}
${lib.getExe' pkgs.coreutils "rm"} -- ${lib.escapeShellArg savedDir}
fi
fi
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
if ${pkgs.findutils}/bin/find ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${pkgs.gnugrep}/bin/grep -q .; then
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
if ${lib.getExe' pkgs.findutils "find"} -- ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${lib.getExe pkgs.gnugrep} -q .; then
${lib.getExe' pkgs.coreutils "rm"} -rf -- ${lib.escapeShellArg savedDir}
else
${pkgs.coreutils}/bin/cp -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
${lib.getExe' pkgs.coreutils "cp"} -a -- ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
${lib.getExe' pkgs.coreutils "rm"} -rf -- ${lib.escapeShellArg savedDir}
fi
fi
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
${pkgs.coreutils}/bin/ln -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
${lib.getExe' pkgs.coreutils "ln"} -s -- ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
fi
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
admin_password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
${lib.getExe pkgs.python3} \
${renderSettingsScript} \
${lib.escapeShellArg defaultSettingsFile} \
${lib.escapeShellArg settingsFile} \
${lib.escapeShellArg service.serverName} \
${lib.escapeShellArg service.passwordFile} \
${lib.escapeShellArg service.adminPasswordFile}
exec ./PalServer.sh \
"$@" \
"-serverpassword=$password" \
"-adminpassword=$admin_password"
exec ./PalServer.sh "$@"
'';
in
{
modules.services.steamcmd = {
enable = true;
inherit (service) steamDir installDir stateDir;
serviceName = "palworld-${lib.toLower service.serverName}";
serviceName = "palworld";
appId = 2394010;
inherit (service) autoStart;
executable = "${startScript}";
arguments = [
"-useperfthreads"
"-NoAsyncLoadingThread"
"-UseMultithreadForDS"
"-port=${toString service.port}"
"-publicport=${toString service.port}"
"-servername=${service.serverName}"
"-players=${toString service.maxPlayers}"
]
++ lib.optional service.publicLobby "-publiclobby"
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
++ lib.optional service.rcon.enable "-rcon";
++ lib.optionals service.publicLobby (
[
"-publiclobby"
"-publicport=${toString service.port}"
]
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
);
udpPorts = [ service.port ];
tcpPorts = [ service.port ];
};
}
);

View File

@@ -51,7 +51,7 @@
startScript = pkgs.writeShellScript "valheim-start" ''
set -eu
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
exec ./valheim_server.x86_64 "$@" -password "$password"
'';
in

View File

@@ -15,7 +15,7 @@
};
serviceName = lib.mkOption {
type = lib.types.str;
type = lib.types.strMatching "[A-Za-z0-9_.@-]+";
example = "valheim";
description = "Name of the systemd service that runs the game server.";
};
@@ -87,10 +87,11 @@
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
set -eu
${pkgs.coreutils}/bin/install -d \
${lib.getExe' pkgs.coreutils "install"} -d \
-o ${user} \
-g ${group} \
-m 0770 \
-- \
${lib.escapeShellArg service.steamDir} \
${lib.escapeShellArg service.installDir} \
${lib.escapeShellArg service.stateDir}
@@ -110,7 +111,7 @@
startScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-start" ''
set -eu
cd ${lib.escapeShellArg service.installDir}
cd -- ${lib.escapeShellArg service.installDir}
export LD_LIBRARY_PATH=linux64:''${LD_LIBRARY_PATH-}
exec ${lib.getExe pkgs.steam-run} ${lib.escapeShellArg service.executable} ${lib.escapeShellArgs service.arguments}
'';
@@ -155,7 +156,7 @@
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.systemd}/bin/systemctl --no-block start ${service.serviceName}.service";
ExecStart = "${lib.getExe' pkgs.systemd "systemctl"} --no-block -- start ${service.serviceName}.service";
};
};

View File

@@ -86,7 +86,7 @@ in
backup = {
description = "Copy <file> to <file>.bak";
argumentNames = "filename";
body = "cp $filename $filename.bak";
body = "cp -- $filename $filename.bak";
};
copy = {

View File

@@ -22,21 +22,21 @@ let
root = root + "/registry";
};
# TODO: Refactor guest-module composition.
guestModules = import ./guests.nix {
inherit
inputs
registry
repositoryModules
specialArgs
util
;
repositoryModules = modules;
root = root + "/guests";
};
modules.imports = [
repositoryModules.imports = [
{
nixpkgs.overlays = [ self.overlays.custom ];
}
registry.module
]
++ util.collectNixFiles (root + "/modules");
@@ -47,8 +47,7 @@ import ./hosts.nix {
lib
specialArgs
guestModules
repositoryModules
;
registryModule = registry.module;
repositoryModules = modules;
root = root + "/hosts";
}

View File

@@ -10,7 +10,12 @@ let
files = util.collectNixFiles root;
mkGuest =
name:
{ config, lib, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.guests.${name};
@@ -18,10 +23,11 @@ let
directoryScript = lib.concatStringsSep "\n" (
lib.mapAttrsToList (_: mount: ''
${config.system.path}/bin/install -d \
${lib.getExe' pkgs.coreutils "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
);
@@ -254,7 +260,6 @@ let
inputs.stylix.nixosModules.stylix
repositoryModules
registry.module
cfg.interior
{

View File

@@ -6,7 +6,6 @@
repositoryModules,
guestModules,
registryModule,
}:
let
names = lib.attrNames directories;
@@ -27,8 +26,6 @@ let
repositoryModules
guestModules
# TODO: refactor
registryModule
(root + "/${name}")
{

View File

@@ -10,6 +10,7 @@ in
{
fix = {
type = "app";
meta.description = "Formats and cleans repository files.";
program = lib.getExe (
pkgs.writeShellApplication {
name = "fix";

View File

@@ -11,3 +11,13 @@ Use `flake.nix` to pass inputs into that implementation.
### Rationale
This keeps dependency declarations separate from output logic.
## Describe flake apps
### Rule
Set `meta.description` on every flake app.
### Rationale
Flake tools can display each app's purpose without emitting a missing-metadata warning.

View File

@@ -50,3 +50,91 @@ Do not read `config.home-manager.users.<user>.programs.<name>.package` for comma
Home Manager uses the NixOS package set in this repository.
`pkgs.<name>` avoids dependency paths through the user tree under Home Manager.
Configured NixOS package options preserve package selection when a module owns the tool.
## Configure services through documented interfaces
### Rule
Use current upstream documentation when mapping Nix options to service arguments or configuration files.
Do not expose undocumented service arguments as module options.
### Rationale
Documented interfaces preserve configuration behavior across service updates.
## Keep module options effective
### Rule
Every custom module option must change the generated configuration or behavior.
Remove unused options instead of exposing speculative configuration.
### Rationale
An inert option promises behavior that the module does not provide.
## Separate public and private addresses
### Rule
Do not assign a private guest address to an option that advertises a public service address.
Leave optional public-address settings unset when the service can detect them.
### Rationale
Private addresses are not routable by public clients.
## Separate display names from identifiers
### Rule
Do not derive systemd unit names or other machine identifiers from unrestricted display names.
Give identifiers constrained types or fixed values.
### Rationale
Display names can contain characters that machine interfaces reject or reinterpret.
## Terminate command options before variable paths
### Rule
In generated shell text, pass `--` before variable-derived path operands when the command supports it.
### Rationale
Values that start with a hyphen remain operands instead of command options.
## Compose repository modules once
### Rule
Add shared NixOS modules to `repositoryModules` in `src/dotfiles/default.nix`.
Import `repositoryModules` into each host and guest.
Do not pass a shared module through a separate parallel parameter.
### Rationale
A single module collection prevents host and guest configurations from diverging.
## Generate identical named attributes
### Rule
Use `lib.genAttrs` when a fixed list of attribute names shares one value.
Do not repeat identical attribute bodies.
### Rationale
One definition prevents copied settings from diverging.
## Import tracked host hardware directly
### Rule
Import each tracked `hardware-configuration.nix` directly from its host module.
Do not guard the import with `builtins.pathExists`.
### Rationale
A missing hardware description must fail evaluation instead of silently changing the host configuration.