diff --git a/fleet/default.nix b/fleet/default.nix new file mode 100644 index 0000000..16e1b7b --- /dev/null +++ b/fleet/default.nix @@ -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; +} diff --git a/fleet/fleet-option.nix b/fleet/fleet-option.nix new file mode 100644 index 0000000..b56a99f --- /dev/null +++ b/fleet/fleet-option.nix @@ -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; }); +}