feat: add fleet configuration options module
- Create fleet/fleet-option.nix to define athenix.fleet and athenix.hwTypes options - Use lib.evalModules to evaluate inventory separately from host configs - Enable external overrides of fleet and hardware types via lib.mkForce - Fix infinite recursion issue by avoiding config references in imports
This commit is contained in:
292
fleet/default.nix
Normal file
292
fleet/default.nix
Normal file
@@ -0,0 +1,292 @@
|
||||
{
|
||||
inputs,
|
||||
fleet ? null,
|
||||
hwTypes ? null,
|
||||
...
|
||||
}:
|
||||
|
||||
# ============================================================================
|
||||
# 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;
|
||||
|
||||
# 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
|
||||
inventoryModule = lib.evalModules {
|
||||
modules = [
|
||||
(import ./fleet-option.nix { inherit inputs; })
|
||||
{ _module.args = { pkgs = nixpkgs.legacyPackages.x86_64-linux; }; }
|
||||
(lib.mkIf (fleet != null) { athenix.fleet = lib.mkForce fleet; })
|
||||
(lib.mkIf (hwTypes != null) { athenix.hwTypes = lib.mkForce hwTypes; })
|
||||
];
|
||||
};
|
||||
|
||||
hostTypes = inventoryModule.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
|
||||
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
|
||||
++ [
|
||||
./common.nix
|
||||
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
|
||||
);
|
||||
|
||||
fleetData = inventoryModule.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;
|
||||
}
|
||||
66
fleet/fleet-option.nix
Normal file
66
fleet/fleet-option.nix
Normal file
@@ -0,0 +1,66 @@
|
||||
# ============================================================================
|
||||
# Fleet Option Definition
|
||||
# ============================================================================
|
||||
# This module only defines the athenix.fleet option without any dependencies.
|
||||
# Used by fleet.nix to evaluate inventory data without circular dependencies.
|
||||
{ inputs, ... }:
|
||||
{ lib, ... }:
|
||||
let
|
||||
fleetDefinition = lib.mkOption {
|
||||
description = "Hardware types definitions for the fleet.";
|
||||
type = lib.types.attrsOf (lib.types.submodule ({ name, ...}: {
|
||||
options = {
|
||||
type = lib.mkOption {
|
||||
type = lib.types.oneOf [
|
||||
lib.types.str
|
||||
lib.types.listOf lib.types.str
|
||||
];
|
||||
default = name;
|
||||
description = "Type(s) of system configuration for this device.";
|
||||
};
|
||||
system = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "x86_64-linux";
|
||||
description = "NixOS system architecture for this hardware type.";
|
||||
};
|
||||
devices = lib.mkOption {
|
||||
type = lib.types.oneOf [
|
||||
lib.types.int
|
||||
(lib.types.attrsOf
|
||||
(lib.types.submodule ({ name, ... }: {
|
||||
freeformType = lib.types.attrs;
|
||||
})
|
||||
))
|
||||
];
|
||||
};
|
||||
count = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 0;
|
||||
description = "Number of devices of this type to create.";
|
||||
};
|
||||
defaultCount = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 0;
|
||||
description = "Default number of devices to create with default configurations and numbered hostnames.";
|
||||
};
|
||||
overrides = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = "Overrides to apply to all devices of this type.";
|
||||
};
|
||||
};
|
||||
}));
|
||||
};
|
||||
in
|
||||
{
|
||||
options.athenix = {
|
||||
fleet = fleetDefinition;
|
||||
hwTypes = lib.mkOption {
|
||||
description = "Hardware types definitions for the fleet.";
|
||||
type = lib.types.attrs;
|
||||
};
|
||||
};
|
||||
|
||||
config.athenix.fleet = lib.mkDefault (import ../inventory.nix);
|
||||
config.athenix.hwTypes = lib.mkDefault (import ../hw { inherit inputs; });
|
||||
}
|
||||
Reference in New Issue
Block a user