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