From 2f8b782a1ff55dc5fe11827f560e6f90670438a8 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 22 Jun 2024 21:31:01 +0200 Subject: [PATCH] Inventory: init module merge & validation logic for inventory --- clanModules/borgbackup/roles/client.nix | 2 +- clanModules/borgbackup/roles/server.nix | 2 +- flakeModules/clan.nix | 2 +- inventory/README.md | 60 ++------- inventory/default.nix | 75 ++--------- lib/build-clan/default.nix | 99 ++++++++------- lib/build-clan/interface.nix | 120 ++++++++++++++++++ lib/build-clan/inventory.nix | 2 +- nixosModules/clanCore/inventory/interface.nix | 4 +- 9 files changed, 198 insertions(+), 168 deletions(-) create mode 100644 lib/build-clan/interface.nix diff --git a/clanModules/borgbackup/roles/client.nix b/clanModules/borgbackup/roles/client.nix index 182e0dc0..84ce41ef 100644 --- a/clanModules/borgbackup/roles/client.nix +++ b/clanModules/borgbackup/roles/client.nix @@ -1,6 +1,6 @@ { config, lib, ... }: let - instances = config.clan.services.borgbackup; + instances = config.clan.inventory.services.borgbackup; # roles = { ${role_name} :: { machines :: [string] } } allServers = lib.foldlAttrs ( acc: _instanceName: instanceConfig: diff --git a/clanModules/borgbackup/roles/server.nix b/clanModules/borgbackup/roles/server.nix index 3885aa31..48c7089c 100644 --- a/clanModules/borgbackup/roles/server.nix +++ b/clanModules/borgbackup/roles/server.nix @@ -4,7 +4,7 @@ let machineDir = clanDir + "/machines/"; inherit (config.clan.core) machineName; - instances = config.clan.services.borgbackup; + instances = config.clan.inventory.services.borgbackup; # roles = { ${role_name} :: { machines :: [string] } } diff --git a/flakeModules/clan.nix b/flakeModules/clan.nix index 139ef8d9..dec466aa 100644 --- a/flakeModules/clan.nix +++ b/flakeModules/clan.nix @@ -13,7 +13,6 @@ let inherit lib clan-core; inherit (inputs) nixpkgs; }; - cfg = config.clan; in { @@ -91,6 +90,7 @@ in clanInternals = lib.mkOption { type = lib.types.submodule { options = { + inventory = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; }; meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; }; all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; }; machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); }; diff --git a/inventory/README.md b/inventory/README.md index 89a1b1e5..4776a388 100644 --- a/inventory/README.md +++ b/inventory/README.md @@ -7,40 +7,18 @@ The inventory is our concept for distributed services. Users can configure multi - Ensures that all machines and services are configured consistently, across multiple nixosConfigs. - Defaults and predefined roles in our modules minimizes the need for manual configuration. -Design questions: +Open questions: + +- [ ] How do we set default role, description and other metadata? + - It must be accessible from Python. + - It must set the value in the module system. + +- [ ] Inventory might use assertions. Should each machine inherit the inventory assertions ? - [ ] Is the service config interface the same as the module config interface ? -- [ ] As a user i dont want to see borgbackup as the high level category ? +- [ ] As a user do I want to see borgbackup as the high level category? -- [x] Must roles be a list ? - -> Yes. In zerotier a machine can be "moon" and "controller" at the same time. - -- [x] Is role client different from peer ? Do we have one example where we use client and peer together and they are different? - -> There are many roles. And they depend on the service. - -- [x] Should we use the module name in the path of the service? - -> YES - ```json - // ${module_name}.${instance_name} - services.borgbackup-static.backup1 = { - - } - ``` - - Pro: - Easier to handle. - Better groups the module specific instances. - Contra: - More nesting in json - - Neutral: Module name is hard to change. Exists anyways. - -- [x] Should the machine specific service config be part of the service? - -> NO. because ... - - The config implements the schema of the module, which is declared in the service. - - If the config is placed in the machine, it becomes unclear that the scope is ONLY the service and NOT the global nixos config. - - If the config is placed in the machine it is de-located into another top-level field. In the module this complicates access. Architecture @@ -55,27 +33,7 @@ nixos < borgbackup <- inventory <-> UI Defining Users is out of scope for the first prototype. ``` -- [ ] Why do we need 2 modules? - -> It is technically possible to have only 1 module. - Pros: - Simple to use/Easy to understand. - Less modules - Cons: - Harder to write a module. Because it must do 2 things. - One module should do only 1 thing. - -```nix -clan.machines.${machine_name} = { - # "borgbackup.ssh.pub" = machineDir + machines + "/facts/borgbackup.ssh.pub"; - facts = ... -}; -clan.services.${instance} = { -# roles.server = [ "jon_machine" ] -# roles.${role_name} = [ ${machine_name} ]; -}; -``` - -This part provides a specification for the inventory. +## Provides a specification for the inventory It is used for design phase and as validation helper. diff --git a/inventory/default.nix b/inventory/default.nix index 4f4f3a9e..a60c392a 100644 --- a/inventory/default.nix +++ b/inventory/default.nix @@ -3,76 +3,25 @@ let clan-core = self; in { - # Extension of the build clan interface - # new_clan = clan-core.lib.buildClan { - # # High level services. - # # If you need multiple instances of a service configure them via: - # # inventory.services.[serviceName].[instanceName] = ... - # services = { - # borbackup = { - # roles.server.machines = [ "vyr" ]; - # roles.client.tags = [ "laptop" ]; - # machines.vyr = { - # config = { - - # }; - # }; - # config = { - - # }; - # }; - # }; - - # # Low level inventory i.e. if you need multiple instances of a service - # # Or if you want to manipulate the created inventory directly. - # inventory.services.borbackup.default = { }; - - # # Machines. each machine can be referenced by its attribute name under services. - # machines = { - # camina = { - # # This is added to machine tags - # clan.tags = [ "laptop" ]; - # # These are the inventory machine fields - # clan.meta.description = ""; - # clan.meta.name = ""; - # clan.meta.icon = ""; - # # Config ... - # }; - # vyr = { - # # Config ... - # }; - # vi = { - # clan.networking.targetHost = "root@78.47.164.46"; - # # Config ... - # }; - # aya = { - # clan.networking.targetHost = "root@78.47.164.46"; - # # Config ... - # }; - # ezra = { - # # Config ... - # }; - # rianon = { - # # Config ... - # }; - # }; - # }; - clan = clan-core.lib.buildClan { - meta.name = "sams's clans"; + + meta.name = "kenjis clan"; # Should usually point to the directory of flake.nix directory = self; - # services = { - # borgbackup = { - # roles.server.machines = [ "vyr_machine" ]; - # roles.client.tags = [ "laptop" ]; - # }; - # }; + # service config + # Useful alias: "inventory.services.borgbackup.default" + services = { + borgbackup = { + roles.server.machines = [ "vyr_machine" ]; + roles.client.tags = [ "laptop" ]; + }; + }; - # OR + # merged with inventory = builtins.fromJSON (builtins.readFile ./src/tests/borgbackup.json); + # merged with machines = { "vyr_machine" = { }; "vi_machine" = { diff --git a/lib/build-clan/default.nix b/lib/build-clan/default.nix index 7907970f..e09975d5 100644 --- a/lib/build-clan/default.nix +++ b/lib/build-clan/default.nix @@ -31,59 +31,62 @@ inventory ? { }, }: let - _inventory = - ( - if services != { } && inventory == { } then - { services = lib.mapAttrs (_name: value: { default = value; }) services; } - else if services == { } && inventory != { } then + # Internal inventory, this is the result of merging all potential inventory sources: + # - Default instances configured via 'services' + # - The inventory overrides + # - Machines that exist in inventory.machines + # - Machines explicitly configured via 'machines' argument + # - Machines that exist in the machines directory + # Checks on the module level: + # - Each service role must reference a valid machine after all machines are merged + mergedInventory = + (lib.evalModules { + modules = [ + ./interface.nix + { inherit meta; } + # Default instances configured via 'services' + { + services = lib.mapAttrs (_name: value: { + default = value // { + meta.name = lib.mkDefault _name; + }; + }) services; + } + # The inventory overrides inventory - else if services != { } && inventory != { } then - throw "Either services or inventory should be set, but not both." - else - { } - ) - // { - machines = lib.mapAttrs ( - name: config: - (lib.attrByPath [ - "clan" - "meta" - ] { } config) - // { - name = ( - lib.attrByPath [ + # Machines explicitly configured via 'machines' argument + { + # { ${name} :: meta // { name, tags } } + machines = lib.mapAttrs ( + name: config: + (lib.attrByPath [ "clan" "meta" - "name" - ] name config - ); - tags = lib.attrByPath [ - "clan" - "tags" - ] [ ] config; + ] { } config) + // { + # meta.name default is the attribute name of the machine + name = lib.mkDefault ( + lib.attrByPath [ + "clan" + "meta" + "name" + ] name config + ); + tags = lib.attrByPath [ + "clan" + "tags" + ] [ ] config; + } + ) machines; } - ) machines; - }; + # Machines that exist in the machines directory + { machines = lib.mapAttrs (name: _: { inherit name; }) machinesDirs; } + ]; + }).config; buildInventory = import ./inventory.nix { inherit lib clan-core; }; - pkgs = import nixpkgs { }; - - inventoryFile = builtins.toFile "inventory.json" (builtins.toJSON _inventory); - - # a Derivation that can be forced to validate the inventory - # It is not used directly here. - validatedFile = pkgs.stdenv.mkDerivation { - name = "validated-inventory"; - src = ../../inventory/src; - buildInputs = [ pkgs.cue ]; - installPhase = '' - cue vet ${inventoryFile} root.cue -d "#Root" - cp ${inventoryFile} $out - ''; - }; - - serviceConfigs = buildInventory _inventory; + serviceConfigs = buildInventory mergedInventory; deprecationWarnings = [ (lib.warnIf ( @@ -167,6 +170,8 @@ let clan-core.nixosModules.clanCore extraConfig (machines.${name} or { }) + # Inherit the inventory assertions ? + { inherit (mergedInventory) assertions; } { imports = serviceConfigs.${name} or { }; } ( { @@ -250,7 +255,7 @@ builtins.deepSeq deprecationWarnings { # Evaluated clan meta # Merged /clan/meta.json with overrides from buildClan meta = mergedMeta; - inherit _inventory validatedFile; + inventory = mergedInventory; # machine specifics machines = configsPerSystem; diff --git a/lib/build-clan/interface.nix b/lib/build-clan/interface.nix new file mode 100644 index 00000000..4a46ae39 --- /dev/null +++ b/lib/build-clan/interface.nix @@ -0,0 +1,120 @@ +{ config, lib, ... }: +let + t = lib.types; + + metaOptions = { + name = lib.mkOption { type = t.str; }; + description = lib.mkOption { + default = null; + type = t.nullOr t.str; + }; + icon = lib.mkOption { + default = null; + type = t.nullOr t.str; + }; + }; + + machineRef = lib.mkOptionType { + name = "machineRef"; + description = "Machine :: [${builtins.concatStringsSep " | " (builtins.attrNames config.machines)}]"; + check = v: lib.isString v && builtins.elem v (builtins.attrNames config.machines); + merge = lib.mergeEqualOption; + }; + + allTags = lib.unique ( + lib.foldlAttrs ( + tags: _: m: + tags ++ m.tags or [ ] + ) [ ] config.machines + ); + + tagRef = lib.mkOptionType { + name = "tagRef"; + description = "Tags :: [${builtins.concatStringsSep " | " allTags}]"; + check = v: lib.isString v && builtins.elem v allTags; + merge = lib.mergeEqualOption; + }; +in +{ + options.assertions = lib.mkOption { + type = t.listOf t.unspecified; + internal = true; + default = [ ]; + }; + config.assertions = lib.foldlAttrs ( + ass1: serviceName: c: + ass1 + ++ lib.foldlAttrs ( + ass2: instanceName: instanceConfig: + let + serviceMachineNames = lib.attrNames instanceConfig.machines; + topLevelMachines = lib.attrNames config.machines; + # All machines must be defined in the top-level machines + assertions = builtins.map (m: { + assertion = builtins.elem m topLevelMachines; + message = "${serviceName}.${instanceName}.machines.${m}. Should be one of [ ${builtins.concatStringsSep " | " topLevelMachines} ]"; + }) serviceMachineNames; + in + ass2 ++ assertions + ) [ ] c + ) [ ] config.services; + + options.meta = metaOptions; + + options.machines = lib.mkOption { + default = { }; + type = t.attrsOf ( + t.submodule { + options = { + inherit (metaOptions) name description icon; + tags = lib.mkOption { + default = [ ]; + apply = lib.unique; + type = t.listOf t.str; + }; + }; + } + ); + }; + + options.services = lib.mkOption { + type = t.attrsOf ( + t.attrsOf ( + t.submodule { + options.meta = metaOptions; + options.config = lib.mkOption { + default = { }; + type = t.anything; + }; + options.machines = lib.mkOption { + default = { }; + type = t.attrsOf ( + t.submodule { + options.config = lib.mkOption { + default = { }; + type = t.anything; + }; + } + ); + }; + options.roles = lib.mkOption { + default = { }; + type = t.attrsOf ( + t.submodule { + options.machines = lib.mkOption { + default = [ ]; + type = t.listOf machineRef; + }; + options.tags = lib.mkOption { + default = [ ]; + apply = lib.unique; + type = t.listOf tagRef; + }; + } + ); + }; + } + ) + ); + }; +} diff --git a/lib/build-clan/inventory.nix b/lib/build-clan/inventory.nix index 075a84d8..a5ab65bc 100644 --- a/lib/build-clan/inventory.nix +++ b/lib/build-clan/inventory.nix @@ -90,7 +90,7 @@ let ]; } { - config.clan.services.${moduleName}.${instanceName} = { + config.clan.inventory.services.${moduleName}.${instanceName} = { roles = resolvedRoles; # TODO: Add inverseRoles to the service config if needed # inherit inverseRoles; diff --git a/nixosModules/clanCore/inventory/interface.nix b/nixosModules/clanCore/inventory/interface.nix index 3a8228d5..895a1599 100644 --- a/nixosModules/clanCore/inventory/interface.nix +++ b/nixosModules/clanCore/inventory/interface.nix @@ -29,9 +29,7 @@ let }; in { - # clan.inventory.${moduleName}.${instanceName} = { ... } - # TODO: resolve clash with clan.services.waypipe - options.clan.services = lib.mkOption { + options.clan.inventory.services = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf instanceOptions); }; }