{ config, options, kubenix, pkgs, lib, ... }: with lib; let cfg = config.submodules; parentConfig = config; matchesVersion = requiredVersion: version: if requiredVersion != null then if hasPrefix "~" requiredVersion then (builtins.match (removePrefix "~" requiredVersion) version) != null else requiredVersion == version else true; getDefaults = { name, version, tags, features }: catAttrs "default" (filter (submoduleDefault: (submoduleDefault.name == null || submoduleDefault.name == name) && (matchesVersion submoduleDefault.version version) && ( (length submoduleDefault.tags == 0) || (length (intersectLists submoduleDefault.tags tags)) > 0 ) && ( (length submoduleDefault.features == 0) || (length (intersectLists submoduleDefault.features features)) > 0 ) ) config.submodules.defaults); specialArgs = cfg.specialArgs // { parentConfig = config; }; findSubmodule = { name, version ? null, latest ? true }: let matchingSubmodules = filter (el: el.definition.name == name && (matchesVersion version el.definition.version) ) cfg.imports; versionSortedSubmodules = sort (s1: s2: if builtins.compareVersions s1.definition.version s2.definition.version > 0 then true else false ) matchingSubmodules; matchingModule = if length versionSortedSubmodules == 0 then throw "No module found ${name}/${if version == null then "latest" else version}" else head versionSortedSubmodules; in matchingModule; passthruConfig = mapAttrsToList (name: opt: { ${name} = mkMerge (mapAttrsToList (_: inst: if inst.passthru.enable then inst.config.submodule.passthru.${name} or { } else { } ) config.submodules.instances); _module.args = mkMerge (mapAttrsToList (_: inst: if inst.passthru.enable then inst.config.submodule.passthru._module.args or { } else { } ) config.submodules.instances); }) (removeAttrs options [ "_definedNames" "_module" "_m" "submodules" ]); submoduleWithSpecialArgs = opts: specialArgs: let opts' = toList opts; inherit (lib.modules) evalModules; in mkOptionType rec { name = "submodule"; check = x: isAttrs x || isFunction x; merge = loc: defs: let coerce = def: if isFunction def then def else { config = def; }; modules = opts' ++ map (def: { _file = def.file; imports = [ (coerce def.value) ]; }) defs; in (evalModules { inherit modules specialArgs; prefix = loc; }).config; getSubOptions = prefix: (evalModules { modules = opts'; inherit prefix specialArgs; # This is a work-around due to the fact that some sub-modules, # such as the one included in an attribute set, expects a "args" # attribute to be given to the sub-module. As the option # evaluation does not have any specific attribute name, we # provide a default one for the documentation. # # This is mandatory as some option declaration might use the # "name" attribute given as argument of the submodule and use it # as the default of option declarations. # # Using lookalike unicode single angle quotation marks because # of the docbook transformation the options receive. In all uses # > and < wouldn't be encoded correctly so the encoded values # would be used, and use of `<` and `>` would break the XML document. # It shouldn't cause an issue since this is cosmetic for the manual. args.name = "‹name›"; }).options; getSubModules = opts'; substSubModules = m: submoduleWithSpecialArgs m specialArgs; functor = (defaultFunctor name) // { # Merging of submodules is done as part of mergeOptionDecls, as we have to annotate # each submodule with its location. payload = [ ]; binOp = lhs: rhs: [ ]; }; }; in { imports = [ ./base.nix ]; options = { submodules.specialArgs = mkOption { description = "Special args to pass to submodules. These arguments can be used for imports"; type = types.attrs; default = { }; }; submodules.defaults = mkOption { description = "List of defaults to apply to submodule instances"; type = types.listOf (types.submodule ({ config, ... }: { options = { name = mkOption { description = "Name of the submodule to apply defaults for"; type = types.nullOr types.str; default = null; }; version = mkOption { description = '' Version of submodule to apply defaults for. If version starts with "~" it is threated as regex pattern for example "~1.0.* ''; type = types.nullOr types.str; default = null; }; tags = mkOption { description = "List of tags to apply defaults for"; type = types.listOf types.str; default = [ ]; }; features = mkOption { description = "List of features that submodule has to have to apply defaults"; type = types.listOf types.str; default = [ ]; }; default = mkOption { description = "Default to apply to submodule instance"; type = types.unspecified; default = { }; }; }; })); default = [ ]; }; submodules.propagate.enable = mkOption { description = "Whether to propagate defaults and imports from parent to child"; type = types.bool; default = true; }; submodules.imports = mkOption { description = "List of submodule imports"; type = types.listOf ( types.coercedTo types.path (module: { inherit module; }) (types.submodule ({ name, config, ... }: let evaledSubmodule' = evalModules { inherit specialArgs; modules = config.modules ++ [ ./base.nix ]; check = false; }; evaledSubmodule = if (!(elem "submodule" evaledSubmodule'.config._m.features)) then throw "no submodule defined" else evaledSubmodule'; in { options = { module = mkOption { description = "Module defining submodule"; type = types.unspecified; }; modules = mkOption { description = "List of modules defining submodule"; type = types.listOf types.unspecified; default = [ config.module ]; }; features = mkOption { description = "List of features exposed by submodule"; type = types.listOf types.str; }; definition = mkOption { description = "Submodule definition"; type = types.attrs; }; exportAs = mkOption { description = "Name under which to register exports"; type = types.nullOr types.str; default = null; }; }; config = { definition = { inherit (evaledSubmodule.config.submodule) name description version tags exports; }; features = evaledSubmodule.config._m.features; }; }) ) ); default = [ ]; }; submodules.instances = mkOption { description = "Attribute set of submodule instances"; default = { }; type = types.attrsOf (types.submodule ({ name, config, options, ... }: let # submodule associated with submodule = findSubmodule { name = config.submodule; version = config.version; }; # definition of a submodule submoduleDefinition = submodule.definition; # submodule defaults defaults = getDefaults { name = submoduleDefinition.name; version = submoduleDefinition.version; tags = submoduleDefinition.tags; features = submodule.features; }; in { options = { name = mkOption { description = "Submodule instance name"; type = types.str; default = name; }; submodule = mkOption { description = "Name of the submodule to use"; type = types.str; default = name; }; version = mkOption { description = '' Version of submodule to use, if version starts with "~" it is threated as regex pattern for example "~1.0.*" ''; type = types.nullOr types.str; default = null; }; passthru.enable = mkOption { description = "Whether to passthru submodule resources"; type = types.bool; default = true; }; config = mkOption { description = "Submodule instance ${config.name} for ${submoduleDefinition.name}:${submoduleDefinition.version} config"; type = submoduleWithSpecialArgs ({ ... }: { imports = submodule.modules ++ defaults ++ [ ./base.nix ]; _module.args.pkgs = pkgs; _module.args.name = config.name; _module.args.submodule = config; submodule.args = mkAliasDefinitions options.args; }) specialArgs; default = { }; }; args = mkOption { description = "Submodule arguments (alias of config.submodule.args)"; }; }; })); }; default = { }; }; config = mkMerge ([ { # register exported functions as args _module.args = mkMerge (map (submodule: { ${submodule.exportAs} = submodule.definition.exports; }) (filter (submodule: submodule.exportAs != null) cfg.imports)); _m.features = [ "submodules" ]; submodules.specialArgs.kubenix = kubenix; # passthru kubenix.project to submodules submodules.defaults = mkMerge [ [{ default = { kubenix.project = parentConfig.kubenix.project; }; }] (map (propagate: { features = propagate.features; default = propagate.module; }) config._m.propagate) ]; } (mkIf cfg.propagate.enable { # if propagate is enabled and submodule has submodules included propagage defaults and imports submodules.defaults = [{ features = [ "submodules" ]; default = { submodules = { defaults = cfg.defaults; imports = cfg.imports; }; }; }]; }) ] ++ passthruConfig); }