fix(nixos): address Nix review findings #183
@@ -12,43 +12,22 @@ in
|
||||
interior = {
|
||||
modules.services.reverse-proxy.enable = true;
|
||||
|
||||
services.nginx.virtualHosts = {
|
||||
"games.alexion.dev" = {
|
||||
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";
|
||||
};
|
||||
"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";
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 2022 ];
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
serverName = "Tracion";
|
||||
autoStart = false;
|
||||
adminPasswordSecret = "services/game-servers/admin/password";
|
||||
publicIP = config.guests.palworld.address;
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
route = {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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.publicLobby (
|
||||
[
|
||||
"-publiclobby"
|
||||
"-publicport=${toString service.port}"
|
||||
]
|
||||
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
||||
++ lib.optional service.rcon.enable "-rcon";
|
||||
);
|
||||
udpPorts = [ service.port ];
|
||||
tcpPorts = [ service.port ];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
repositoryModules,
|
||||
|
||||
guestModules,
|
||||
registryModule,
|
||||
}:
|
||||
let
|
||||
names = lib.attrNames directories;
|
||||
@@ -27,8 +26,6 @@ let
|
||||
repositoryModules
|
||||
guestModules
|
||||
|
||||
# TODO: refactor
|
||||
registryModule
|
||||
(root + "/${name}")
|
||||
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ in
|
||||
{
|
||||
fix = {
|
||||
type = "app";
|
||||
meta.description = "Formats and cleans repository files.";
|
||||
program = lib.getExe (
|
||||
pkgs.writeShellApplication {
|
||||
name = "fix";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user