refactor: create glue/ and variants/ directories
- Add glue/ for fleet generation logic and common configuration - Add variants/ for hardware type modules - Improves separation of concerns and module organization
This commit is contained in:
278
glue/fleet.nix
Normal file
278
glue/fleet.nix
Normal file
@@ -0,0 +1,278 @@
|
||||
{
|
||||
inputs,
|
||||
fleet ? import ../inventory.nix,
|
||||
...
|
||||
}:
|
||||
|
||||
# ============================================================================
|
||||
# Fleet Generator
|
||||
# ============================================================================
|
||||
# This file contains the logic to generate NixOS configurations for all hosts
|
||||
# defined in inventory.nix. It supports both hostname-based and count-based
|
||||
# configurations with flexible type associations.
|
||||
|
||||
let
|
||||
nixpkgs = inputs.nixpkgs;
|
||||
lib = nixpkgs.lib;
|
||||
|
||||
# Load all available host types from hosts/
|
||||
hostTypes = import ../variants { inherit inputs; };
|
||||
|
||||
# Helper to create a single NixOS system configuration
|
||||
mkHost =
|
||||
{
|
||||
hostName,
|
||||
system ? "x86_64-linux",
|
||||
hostType,
|
||||
configOverrides ? { },
|
||||
externalModuleThunk ? null,
|
||||
}:
|
||||
let
|
||||
# Lazy evaluation: only fetch external module when building this host
|
||||
externalModulePath =
|
||||
if externalModuleThunk != null then
|
||||
let
|
||||
# Force evaluation of the thunk (fetchGit, fetchTarball, etc.)
|
||||
fetchedPath = externalModuleThunk;
|
||||
# Extract outPath from fetchGit/fetchTarball results
|
||||
extractedPath =
|
||||
if builtins.isAttrs fetchedPath && fetchedPath ? outPath then fetchedPath.outPath else fetchedPath;
|
||||
in
|
||||
if builtins.isPath extractedPath then
|
||||
extractedPath + "/default.nix"
|
||||
else if lib.isDerivation extractedPath then
|
||||
extractedPath + "/default.nix"
|
||||
else
|
||||
extractedPath + "/default.nix"
|
||||
else
|
||||
null;
|
||||
|
||||
# Load users.nix to find external user modules
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
usersData = import ../users.nix { inherit pkgs; };
|
||||
accounts = usersData.athenix.users or { };
|
||||
|
||||
# Build a map of user names to their nixos module paths (if they exist)
|
||||
# We'll use this to conditionally import modules based on user.enable
|
||||
userNixosModulePaths = lib.filterAttrs (_: v: v != null) (
|
||||
lib.mapAttrs (
|
||||
name: user:
|
||||
if (user ? external && user.external != null) then
|
||||
let
|
||||
externalPath =
|
||||
if builtins.isAttrs user.external && user.external ? outPath then
|
||||
user.external.outPath
|
||||
else
|
||||
user.external;
|
||||
nixosModulePath = externalPath + "/nixos.nix";
|
||||
in
|
||||
if
|
||||
(builtins.isPath externalPath || (builtins.isString externalPath && lib.hasPrefix "/" externalPath))
|
||||
&& builtins.pathExists nixosModulePath
|
||||
then
|
||||
nixosModulePath
|
||||
else
|
||||
null
|
||||
else
|
||||
null
|
||||
) accounts
|
||||
);
|
||||
|
||||
# Create conditional wrapper modules for each user's nixos.nix
|
||||
# Each wrapper checks if the user is enabled before applying the module content
|
||||
userNixosModules = lib.mapAttrsToList (
|
||||
name: modulePath:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}@args:
|
||||
let
|
||||
# Import the user's nixos module - it returns a function or attrset
|
||||
importedModuleFunc = import modulePath { inherit inputs; };
|
||||
# If it's a function, call it with the module args; otherwise use as-is
|
||||
importedModule =
|
||||
if lib.isFunction importedModuleFunc then importedModuleFunc args else importedModuleFunc;
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (config.athenix.users.${name}.enable or false) importedModule;
|
||||
}
|
||||
) userNixosModulePaths;
|
||||
|
||||
# Get the host type module from the hostTypes attribute set
|
||||
typeModule =
|
||||
hostTypes.${hostType}
|
||||
or (throw "Host type '${hostType}' not found. Available types: ${lib.concatStringsSep ", " (lib.attrNames hostTypes)}");
|
||||
|
||||
# External module from fetchGit/fetchurl
|
||||
externalPathModule =
|
||||
if externalModulePath != null then import externalModulePath { inherit inputs; } else { };
|
||||
|
||||
# Config override module - translate special keys to athenix options
|
||||
overrideModule =
|
||||
{ ... }:
|
||||
let
|
||||
cleanConfig = lib.removeAttrs configOverrides [
|
||||
"type"
|
||||
"count"
|
||||
"devices"
|
||||
"overrides"
|
||||
"defaultCount"
|
||||
"buildMethods"
|
||||
];
|
||||
specialConfig = lib.optionalAttrs (configOverrides ? buildMethods) {
|
||||
athenix.host.buildMethods = configOverrides.buildMethods;
|
||||
};
|
||||
in
|
||||
{
|
||||
config = lib.mkMerge [
|
||||
cleanConfig
|
||||
specialConfig
|
||||
];
|
||||
};
|
||||
|
||||
allModules =
|
||||
userNixosModules
|
||||
++ [
|
||||
(import ./common.nix { inherit inputs; })
|
||||
typeModule
|
||||
overrideModule
|
||||
{ networking.hostName = hostName; }
|
||||
]
|
||||
++ lib.optional (externalModulePath != null) externalPathModule;
|
||||
in
|
||||
{
|
||||
system = lib.nixosSystem {
|
||||
inherit system;
|
||||
specialArgs = { inherit inputs; };
|
||||
modules = allModules;
|
||||
};
|
||||
modules = allModules;
|
||||
};
|
||||
|
||||
# Process inventory entries - top-level keys are always prefixes
|
||||
processInventory = lib.mapAttrs (
|
||||
prefix: config:
|
||||
let
|
||||
hostType = config.type or prefix;
|
||||
system = config.system or "x86_64-linux";
|
||||
devices = config.devices or { };
|
||||
hasCount = config ? count;
|
||||
|
||||
# Helper to generate hostname from prefix and suffix
|
||||
# Numbers get no dash: "nix-surface1", "nix-surface2"
|
||||
# Letters get dash: "nix-surface-alpha", "nix-surface-beta"
|
||||
mkHostName =
|
||||
prefix: suffix: usePrefix:
|
||||
if !usePrefix then
|
||||
suffix
|
||||
else if lib.match "[0-9]+" suffix != null then
|
||||
"${prefix}${suffix}" # numeric: no dash
|
||||
else
|
||||
"${prefix}-${suffix}"; # non-numeric: add dash
|
||||
|
||||
# Extract common overrides and default count
|
||||
overrides = config.overrides or { };
|
||||
defaultCount = config.defaultCount or 0;
|
||||
|
||||
# If devices is a number, treat it as count
|
||||
devicesValue = config.devices or { };
|
||||
actualDevices = if lib.isInt devicesValue then { } else devicesValue;
|
||||
actualCount = if lib.isInt devicesValue then devicesValue else (config.count or 0);
|
||||
|
||||
# Clean base config - remove inventory management keys
|
||||
baseConfig = lib.removeAttrs config [
|
||||
"type"
|
||||
"system"
|
||||
"count"
|
||||
"devices"
|
||||
"overrides"
|
||||
"defaultCount"
|
||||
];
|
||||
|
||||
# Generate hosts from explicit device definitions
|
||||
deviceHosts = lib.listToAttrs (
|
||||
lib.mapAttrsToList (
|
||||
deviceKey: deviceConfig:
|
||||
let
|
||||
# Check if deviceConfig has an 'external' field for lazy evaluation
|
||||
hasExternalField = builtins.isAttrs deviceConfig && deviceConfig ? external;
|
||||
|
||||
# Extract external module thunk if present (don't evaluate yet!)
|
||||
externalModuleThunk = if hasExternalField then deviceConfig.external else null;
|
||||
|
||||
# Remove 'external' from config to avoid conflicts
|
||||
cleanDeviceConfig =
|
||||
if hasExternalField then lib.removeAttrs deviceConfig [ "external" ] else deviceConfig;
|
||||
|
||||
# Merge: base config -> overrides -> device-specific config
|
||||
mergedConfig = lib.recursiveUpdate (lib.recursiveUpdate baseConfig overrides) cleanDeviceConfig;
|
||||
|
||||
# Check useHostPrefix from the merged config
|
||||
usePrefix = mergedConfig.athenix.host.useHostPrefix or true;
|
||||
hostName = mkHostName prefix deviceKey usePrefix;
|
||||
in
|
||||
{
|
||||
name = hostName;
|
||||
value = mkHost {
|
||||
inherit
|
||||
hostName
|
||||
system
|
||||
hostType
|
||||
externalModuleThunk
|
||||
;
|
||||
configOverrides = mergedConfig;
|
||||
};
|
||||
}
|
||||
) actualDevices
|
||||
);
|
||||
|
||||
# Generate numbered hosts from count or defaultCount
|
||||
# If devices are specified, defaultCount fills in the gaps
|
||||
countToUse = if actualCount > 0 then actualCount else defaultCount;
|
||||
|
||||
# Get which numbered keys are already defined in devices
|
||||
existingNumbers = lib.filter (k: lib.match "[0-9]+" k != null) (lib.attrNames actualDevices);
|
||||
|
||||
countHosts =
|
||||
if countToUse > 0 then
|
||||
lib.listToAttrs (
|
||||
lib.filter (x: x != null) (
|
||||
map (
|
||||
i:
|
||||
let
|
||||
deviceKey = toString i;
|
||||
# Skip if this number is already in explicit devices
|
||||
alreadyDefined = lib.elem deviceKey existingNumbers;
|
||||
in
|
||||
if alreadyDefined then
|
||||
null
|
||||
else
|
||||
let
|
||||
hostName = mkHostName prefix deviceKey true;
|
||||
mergedConfig = lib.recursiveUpdate baseConfig overrides;
|
||||
in
|
||||
{
|
||||
name = hostName;
|
||||
value = mkHost {
|
||||
inherit hostName system hostType;
|
||||
configOverrides = mergedConfig;
|
||||
};
|
||||
}
|
||||
) (lib.range 1 countToUse)
|
||||
)
|
||||
)
|
||||
else
|
||||
{ };
|
||||
in
|
||||
lib.recursiveUpdate deviceHosts countHosts
|
||||
) fleet;
|
||||
|
||||
# Flatten the nested structure
|
||||
allHosts = lib.foldl' lib.recursiveUpdate { } (lib.attrValues processInventory);
|
||||
in
|
||||
{
|
||||
nixosConfigurations = lib.mapAttrs (n: v: v.system) allHosts;
|
||||
modules = lib.mapAttrs (n: v: v.modules) allHosts;
|
||||
}
|
||||
Reference in New Issue
Block a user