chore: split nixos and opentofu

This commit is contained in:
Tibo De Peuter 2026-07-24 22:17:43 +02:00
parent d125848b82
commit 06500a8f01
Signed by: tdpeuter
SSH key fingerprint: SHA256:u/h/LVoqKF1Iz02uOyxe6hcjmoZASCGV2HM0TG9ZMoU
68 changed files with 44 additions and 61 deletions

View file

@ -0,0 +1,26 @@
{
imports = [
./docker.nix
./gitops.nix
./monitoring.nix
./nfs.nix
./traefik.nix
./users.nix
];
config = {
homelab = {
services.openssh.enable = true;
users.admin.enable = true;
gitops.enable = true;
};
nix.settings.experimental-features = [
"flakes"
"nix-command"
];
# Set your time zone.
time.timeZone = "Europe/Brussels";
};
}

View file

@ -0,0 +1,38 @@
{ config, lib, pkgs, ... }:
{
options.homelab.dockerNetworks = lib.mkOption {
description = "Declarative Docker networks to create before containers start.";
default = {};
type = lib.types.attrsOf (lib.types.submodule {
options = {
requiredBy = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "List of systemd services that require this network (e.g., docker-containerName.service).";
};
};
});
};
config = {
systemd.services = lib.mapAttrs' (networkName: cfg:
lib.nameValuePair "docker-${networkName}-create-network" {
description = "Create Docker network for ${networkName}";
requiredBy = cfg.requiredBy;
after = [ "network.target" "docker.service" ];
requires = [ "docker.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "create-${networkName}-docker-network" ''
if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then
${pkgs.docker}/bin/docker network create ${networkName}
fi
'';
};
}
) config.homelab.dockerNetworks;
};
}

View file

@ -0,0 +1,141 @@
{ config, lib, pkgs, ... }:
let
cfg = config.homelab.gitops;
updateScript = pkgs.writeShellApplication {
name = "homelab-gitops-update";
runtimeInputs = [ pkgs.git pkgs.nixos-rebuild pkgs.jq pkgs.coreutils ];
text = ''
set -euo pipefail
REMOTE_URL="${cfg.repoUrl}"
BRANCH="${cfg.branch}"
echo "Checking remote hash for $REMOTE_URL branch $BRANCH..."
# Fetch remote hash, fallback to unknown if it fails
REMOTE_HASH=$(git ls-remote "$REMOTE_URL" "refs/heads/$BRANCH" | awk '{print $1}' || true)
if [ -z "$REMOTE_HASH" ]; then
echo "WARNING: Could not fetch remote hash. Forcing rebuild to be safe."
REMOTE_HASH="unknown_remote"
fi
LOCAL_HASH="unknown_local"
if [ -f /run/current-system/configurationRevision ]; then
LOCAL_HASH=$(cat /run/current-system/configurationRevision)
fi
echo "Remote hash: $REMOTE_HASH"
echo "Local hash: $LOCAL_HASH"
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ] && [ "$REMOTE_HASH" != "unknown_remote" ] && [ "$LOCAL_HASH" != "unknown" ]; then
echo "Hashes match. No update needed."
exit 0
fi
echo "Hashes differ or unknown. Triggering nixos-rebuild..."
# Trigger the build and switch
nixos-rebuild switch --flake "git+$REMOTE_URL?dir=nixos&ref=$BRANCH"
echo "Update successful."
'';
};
in {
options.homelab.gitops = {
enable = lib.mkEnableOption "Custom GitOps native deployment system";
repoUrl = lib.mkOption {
type = lib.types.str;
default = "https://git.depeuter.dev/Bos55/nix-config.git";
description = "The repository URL to pull configurations from.";
};
branch = lib.mkOption {
type = lib.types.str;
default = "v2";
description = "The branch to deploy.";
};
useBuilder = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to use the central Builder host to compile packages.";
};
};
config = lib.mkIf cfg.enable {
# 1. Systemd Service and Timer for polling
systemd.services.homelab-gitops = {
description = "Homelab GitOps Update Service";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${updateScript}/bin/homelab-gitops-update";
# Must run as root to rebuild the system
User = "root";
};
};
systemd.timers.homelab-gitops = {
description = "Timer for Homelab GitOps Update Service";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5m";
OnUnitActiveSec = "5m";
RandomizedDelaySec = "30s";
};
};
# 2. Webhook listener for instant trigger
sops.secrets."webhook-secret" = {};
services.webhook = {
enable = true;
port = 9000;
hooks = {
gitops = {
execute-command = "${pkgs.systemd}/bin/systemctl";
pass-arguments-to-command = [
{ source = "string"; name = "start"; }
{ source = "string"; name = "homelab-gitops.service"; }
];
trigger-rule = {
match = {
type = "payload-hash-sha256";
secret = "{{ getenv \"WEBHOOK_SECRET\" }}";
parameter = {
source = "header";
name = "X-Forgejo-Signature";
};
};
};
};
};
};
# Inject the secret as an environment variable into the webhook service
systemd.services.webhook.serviceConfig.EnvironmentFile = config.sops.secrets."webhook-secret".path;
# 3. Builder Configuration
sops.secrets."builder-ssh-key" = lib.mkIf cfg.useBuilder {};
nix.buildMachines = lib.mkIf cfg.useBuilder [
{
hostName = "builder.depeuter.dev"; # Must be routable from nodes
system = "x86_64-linux";
sshUser = "builder";
sshKey = config.sops.secrets."builder-ssh-key".path;
maxJobs = 4;
speedFactor = 2;
supportedFeatures = [ "nixos-test" "benchmark" "big-parallel" "kvm" ];
}
];
nix.distributedBuilds = lib.mkIf cfg.useBuilder true;
};
}

View file

@ -0,0 +1,47 @@
{ config, lib, pkgs, ... }:
{
# Prometheus Node Exporter for hardware metrics
services.prometheus.exporters = {
node = {
enable = true;
enabledCollectors = [ "systemd" ];
port = 9100;
};
};
# Promtail to ship logs to Loki
services.promtail = {
enable = true;
configuration = {
server = {
http_listen_port = 28183;
grpc_listen_port = 0;
};
positions = {
filename = "/tmp/positions.yaml";
};
clients = [{
# Use the internal DNS name for the Loki ingress
url = "http://loki.lab.depeuter.dev/loki/api/v1/push";
}];
scrape_configs = [{
job_name = "journal";
journal = {
max_age = "12h";
labels = {
job = "systemd-journal";
host = config.networking.hostName;
};
};
relabel_configs = [{
source_labels = [ "__journal__systemd_unit" ];
target_label = "unit";
}];
}];
};
};
# Open firewall ports for node-exporter so Prometheus can scrape it
networking.firewall.allowedTCPPorts = [ 9100 ];
}

View file

@ -0,0 +1,46 @@
{ config, lib, ... }:
{
options.homelab.nfsMounts = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
device = lib.mkOption {
type = lib.types.str;
description = "The NFS device, e.g. 192.168.0.11:/mnt/POOL/DATA";
};
readOnly = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to mount the NFS share read-only";
};
extraOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"async"
"soft"
"timeo=100"
"retry=50"
"actimeo=1800"
"lookupcache=all"
];
description = "Extra NFS mount options to append";
};
};
});
default = {};
description = "NFS mounts to automatically configure with standard homelab options";
};
config = {
fileSystems = lib.mapAttrs (path: cfg: {
device = cfg.device;
fsType = "nfs";
options = [
"nfsvers=4.2"
"nosuid"
"tcp"
] ++ (if cfg.readOnly then [ "ro" ] else [ "rw" ])
++ cfg.extraOptions;
}) config.homelab.nfsMounts;
};
}

View file

@ -0,0 +1,65 @@
{ config, lib, ... }:
let
proxyNet = config.homelab.apps.traefik.sharedNetworkName;
in {
options.homelab.traefikRouters = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
rule = lib.mkOption {
type = lib.types.str;
description = "The Traefik router rule, e.g. Host(`example.com`)";
};
port = lib.mkOption {
type = lib.types.int;
description = "The port the service listens on";
};
middlewares = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Middlewares to apply";
};
tls = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable TLS";
};
entryPoints = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Entrypoints to use";
};
extraLabels = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = {};
description = "Extra labels to apply";
};
};
});
default = {};
description = "Declarative Traefik router configuration";
};
config = {
# Generate labels for containers based on homelab.traefikRouters
# This assumes that the name in homelab.traefikRouters matches the container name.
virtualisation.oci-containers.containers = lib.mapAttrs (name: router: {
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.${name}.rule" = router.rule;
"traefik.http.services.${name}.loadbalancer.server.port" = toString router.port;
}
// lib.optionalAttrs (router.middlewares != []) {
"traefik.http.routers.${name}.middlewares" = builtins.concatStringsSep "," router.middlewares;
}
// lib.optionalAttrs router.tls {
"traefik.http.routers.${name}.tls" = "true";
}
// lib.optionalAttrs (router.entryPoints != []) {
"traefik.http.routers.${name}.entryPoints" = builtins.concatStringsSep "," router.entryPoints;
}
// router.extraLabels;
}) config.homelab.traefikRouters;
};
}

View file

@ -0,0 +1,37 @@
{ config, lib, ... }:
{
options.homelab.appUsers = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
uid = lib.mkOption {
type = lib.types.int;
description = "The user ID for the app user";
};
group = lib.mkOption {
type = lib.types.str;
default = config.users.groups.apps.name;
description = "The primary group for the app user";
};
extraGroups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Extra groups for the app user";
};
};
});
default = {};
description = "App users to automatically create with standard homelab options";
};
config = {
users.users = lib.mapAttrs (name: cfg: {
uid = lib.mkForce cfg.uid;
isSystemUser = true;
group = cfg.group;
extraGroups = cfg.extraGroups;
home = "/var/empty";
shell = null;
}) config.homelab.appUsers;
};
}