All checks were successful
CI / Format Check (push) Successful in 2s
CI / Flake Check (push) Successful in 1m34s
CI / Evaluate Key Configurations (nix-builder) (push) Successful in 8s
CI / Evaluate Key Configurations (nix-desktop1) (push) Successful in 6s
CI / Evaluate Key Configurations (nix-laptop1) (push) Successful in 6s
CI / Evaluate Artifacts (installer-iso-nix-laptop1) (push) Successful in 13s
CI / Evaluate Artifacts (lxc-nix-builder) (push) Successful in 7s
287 lines
9.5 KiB
Nix
287 lines
9.5 KiB
Nix
{
|
|
inputs,
|
|
lib,
|
|
config,
|
|
self ? null,
|
|
users ? { },
|
|
...
|
|
}:
|
|
|
|
# ============================================================================
|
|
# 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
|
|
|
|
# Evaluate inventory to get fleet data
|
|
# Import fleet-option.nix (defines athenix.fleet) and inventory.nix (sets values)
|
|
# We use a minimal module here to avoid circular dependencies from common.nix's imports
|
|
|
|
hostTypes = config.athenix.hwTypes;
|
|
|
|
# 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
|
|
accounts = config.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
|
|
++ [
|
|
./common.nix
|
|
typeModule
|
|
overrideModule
|
|
{ networking.hostName = hostName; }
|
|
{
|
|
# Inject user definitions from flake-parts level
|
|
config.athenix.users = lib.mapAttrs (_: user: lib.mapAttrs (_: lib.mkDefault) user) users;
|
|
}
|
|
]
|
|
++ lib.optional (externalModulePath != null) externalPathModule;
|
|
in
|
|
{
|
|
system = lib.nixosSystem {
|
|
inherit system;
|
|
specialArgs = {
|
|
inputs = if self != null then inputs // { inherit self; } else 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";
|
|
|
|
# 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
|
|
);
|
|
|
|
fleetData = config.athenix.fleet;
|
|
|
|
# Flatten the nested structure
|
|
allHosts = lib.foldl' lib.recursiveUpdate { } (lib.attrValues (processInventory fleetData));
|
|
in
|
|
{
|
|
nixosConfigurations = lib.mapAttrs (n: v: v.system) allHosts;
|
|
modules = lib.mapAttrs (n: v: v.modules) allHosts;
|
|
}
|