Merge pull request 'clanModules: remove unused code' (#4785) from clean-dead-code into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4785
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,7 +39,6 @@ select
|
|||||||
# Generated files
|
# Generated files
|
||||||
pkgs/clan-app/ui/api/API.json
|
pkgs/clan-app/ui/api/API.json
|
||||||
pkgs/clan-app/ui/api/API.ts
|
pkgs/clan-app/ui/api/API.ts
|
||||||
pkgs/clan-app/ui/api/Inventory.ts
|
|
||||||
pkgs/clan-app/ui/api/modules_schemas.json
|
pkgs/clan-app/ui/api/modules_schemas.json
|
||||||
pkgs/clan-app/ui/api/schema.json
|
pkgs/clan-app/ui/api/schema.json
|
||||||
pkgs/clan-app/ui/.fonts
|
pkgs/clan-app/ui/.fonts
|
||||||
|
|||||||
@@ -139,33 +139,6 @@ in
|
|||||||
nixosTests
|
nixosTests
|
||||||
// flakeOutputs
|
// flakeOutputs
|
||||||
// {
|
// {
|
||||||
# TODO: Automatically provide this check to downstream users to check their modules
|
|
||||||
clan-modules-json-compatible =
|
|
||||||
let
|
|
||||||
allSchemas = lib.mapAttrs (
|
|
||||||
_n: m:
|
|
||||||
let
|
|
||||||
schema =
|
|
||||||
(self.clanLib.evalService {
|
|
||||||
modules = [ m ];
|
|
||||||
prefix = [
|
|
||||||
"checks"
|
|
||||||
system
|
|
||||||
];
|
|
||||||
}).config.result.api.schema;
|
|
||||||
in
|
|
||||||
schema
|
|
||||||
) self.clan.modules;
|
|
||||||
in
|
|
||||||
pkgs.runCommand "combined-result"
|
|
||||||
{
|
|
||||||
schemaFile = builtins.toFile "schemas.json" (builtins.toJSON allSchemas);
|
|
||||||
}
|
|
||||||
''
|
|
||||||
mkdir -p $out
|
|
||||||
cat $schemaFile > $out/allSchemas.json
|
|
||||||
'';
|
|
||||||
|
|
||||||
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } ''
|
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } ''
|
||||||
cp -r ${privateInputs.clan-core-for-checks} $out
|
cp -r ${privateInputs.clan-core-for-checks} $out
|
||||||
chmod -R +w $out
|
chmod -R +w $out
|
||||||
|
|||||||
@@ -18,27 +18,8 @@
|
|||||||
inherit (self) clanModules;
|
inherit (self) clanModules;
|
||||||
clan-core = self;
|
clan-core = self;
|
||||||
inherit pkgs;
|
inherit pkgs;
|
||||||
evalClanModules = self.clanLib.evalClan.evalClanModules;
|
|
||||||
modulesRolesOptions = self.clanLib.evalClan.evalClanModulesWithRoles {
|
|
||||||
allModules = self.clanModules;
|
|
||||||
inherit pkgs;
|
|
||||||
clan-core = self;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# Frontmatter for clanModules
|
|
||||||
clanModulesFrontmatter =
|
|
||||||
let
|
|
||||||
docs = pkgs.nixosOptionsDoc {
|
|
||||||
options = self.clanLib.modules.frontmatterOptions;
|
|
||||||
transformOptions = self.clanLib.docs.stripStorePathsFromDeclarations;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
docs.optionsJSON;
|
|
||||||
|
|
||||||
# Options available when imported via ` inventory.${moduleName}....${rolesName} `
|
|
||||||
clanModulesViaRoles = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaRoles);
|
|
||||||
|
|
||||||
# clan service options
|
# clan service options
|
||||||
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
|
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
|
||||||
|
|
||||||
@@ -88,12 +69,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
||||||
|
|
||||||
# A file that contains the links to all clanModule docs
|
# A file that contains the links to all clanModule docs
|
||||||
export CLAN_MODULES_VIA_ROLES=${clanModulesViaRoles}
|
|
||||||
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
|
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
|
||||||
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
|
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
|
||||||
# Frontmatter format for clanModules
|
|
||||||
export CLAN_MODULES_FRONTMATTER_DOCS=${clanModulesFrontmatter}/share/doc/nixos/options.json
|
|
||||||
|
|
||||||
export BUILD_CLAN_PATH=${buildClanOptions}/share/doc/nixos/options.json
|
export BUILD_CLAN_PATH=${buildClanOptions}/share/doc/nixos/options.json
|
||||||
|
|
||||||
@@ -107,7 +86,6 @@
|
|||||||
legacyPackages = {
|
legacyPackages = {
|
||||||
inherit
|
inherit
|
||||||
jsonDocs
|
jsonDocs
|
||||||
clanModulesViaRoles
|
|
||||||
clanModulesViaService
|
clanModulesViaService
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
modulesRolesOptions,
|
|
||||||
nixosOptionsDoc,
|
nixosOptionsDoc,
|
||||||
evalClanModules,
|
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
clan-core,
|
clan-core,
|
||||||
@@ -10,21 +8,36 @@
|
|||||||
let
|
let
|
||||||
inherit (clan-core.clanLib.docs) stripStorePathsFromDeclarations;
|
inherit (clan-core.clanLib.docs) stripStorePathsFromDeclarations;
|
||||||
transformOptions = stripStorePathsFromDeclarations;
|
transformOptions = stripStorePathsFromDeclarations;
|
||||||
|
|
||||||
|
nixosConfigurationWithClan =
|
||||||
|
let
|
||||||
|
evaled = lib.evalModules {
|
||||||
|
class = "nixos";
|
||||||
|
modules = [
|
||||||
|
# Basemodule
|
||||||
|
(
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
imports = (import (pkgs.path + "/nixos/modules/module-list.nix"));
|
||||||
|
nixpkgs.pkgs = pkgs;
|
||||||
|
clan.core.name = "dummy";
|
||||||
|
system.stateVersion = config.system.nixos.release;
|
||||||
|
# Set this to work around a bug where `clan.core.settings.machine.name`
|
||||||
|
# is forced due to `networking.interfaces` being forced
|
||||||
|
# somewhere in the nixpkgs options
|
||||||
|
facter.detected.dhcp.enable = lib.mkForce false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
{
|
||||||
|
clan.core.settings.directory = clan-core;
|
||||||
|
}
|
||||||
|
clan-core.nixosModules.clanCore
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
evaled;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
||||||
clanModulesViaRoles = lib.mapAttrs (
|
|
||||||
_moduleName: rolesOptions:
|
|
||||||
lib.mapAttrs (
|
|
||||||
_roleName: options:
|
|
||||||
(nixosOptionsDoc {
|
|
||||||
inherit options;
|
|
||||||
warningsAreErrors = true;
|
|
||||||
inherit transformOptions;
|
|
||||||
}).optionsJSON
|
|
||||||
) rolesOptions
|
|
||||||
) modulesRolesOptions;
|
|
||||||
|
|
||||||
# Test with:
|
# Test with:
|
||||||
# nix build .\#legacyPackages.x86_64-linux.clanModulesViaService
|
# nix build .\#legacyPackages.x86_64-linux.clanModulesViaService
|
||||||
clanModulesViaService = lib.mapAttrs (
|
clanModulesViaService = lib.mapAttrs (
|
||||||
@@ -38,7 +51,6 @@ in
|
|||||||
{
|
{
|
||||||
roles = lib.mapAttrs (
|
roles = lib.mapAttrs (
|
||||||
_roleName: role:
|
_roleName: role:
|
||||||
|
|
||||||
(nixosOptionsDoc {
|
(nixosOptionsDoc {
|
||||||
transformOptions =
|
transformOptions =
|
||||||
opt:
|
opt:
|
||||||
@@ -54,20 +66,13 @@ in
|
|||||||
warningsAreErrors = true;
|
warningsAreErrors = true;
|
||||||
}).optionsJSON
|
}).optionsJSON
|
||||||
) evaluatedService.config.roles;
|
) evaluatedService.config.roles;
|
||||||
|
|
||||||
manifest = evaluatedService.config.manifest;
|
manifest = evaluatedService.config.manifest;
|
||||||
|
|
||||||
}
|
}
|
||||||
) clan-core.clan.modules;
|
) clan-core.clan.modules;
|
||||||
|
|
||||||
clanCore =
|
clanCore =
|
||||||
(nixosOptionsDoc {
|
(nixosOptionsDoc {
|
||||||
options =
|
options = nixosConfigurationWithClan.options.clan.core;
|
||||||
((evalClanModules {
|
|
||||||
modules = [ ];
|
|
||||||
inherit pkgs clan-core;
|
|
||||||
}).options
|
|
||||||
).clan.core or { };
|
|
||||||
warningsAreErrors = true;
|
warningsAreErrors = true;
|
||||||
inherit transformOptions;
|
inherit transformOptions;
|
||||||
}).optionsJSON;
|
}).optionsJSON;
|
||||||
|
|||||||
@@ -33,22 +33,13 @@ from clan_lib.errors import ClanError
|
|||||||
from clan_lib.services.modules import (
|
from clan_lib.services.modules import (
|
||||||
CategoryInfo,
|
CategoryInfo,
|
||||||
Frontmatter,
|
Frontmatter,
|
||||||
extract_frontmatter,
|
|
||||||
get_roles,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get environment variables
|
# Get environment variables
|
||||||
CLAN_CORE_PATH = Path(os.environ["CLAN_CORE_PATH"])
|
CLAN_CORE_PATH = Path(os.environ["CLAN_CORE_PATH"])
|
||||||
CLAN_CORE_DOCS = Path(os.environ["CLAN_CORE_DOCS"])
|
CLAN_CORE_DOCS = Path(os.environ["CLAN_CORE_DOCS"])
|
||||||
CLAN_MODULES_FRONTMATTER_DOCS = os.environ.get("CLAN_MODULES_FRONTMATTER_DOCS")
|
|
||||||
BUILD_CLAN_PATH = os.environ.get("BUILD_CLAN_PATH")
|
BUILD_CLAN_PATH = os.environ.get("BUILD_CLAN_PATH")
|
||||||
|
|
||||||
## Clan modules ##
|
|
||||||
# Some modules can be imported via nix natively
|
|
||||||
CLAN_MODULES_VIA_NIX = os.environ.get("CLAN_MODULES_VIA_NIX")
|
|
||||||
# Some modules can be imported via inventory
|
|
||||||
CLAN_MODULES_VIA_ROLES = os.environ.get("CLAN_MODULES_VIA_ROLES")
|
|
||||||
|
|
||||||
# Options how to author clan.modules
|
# Options how to author clan.modules
|
||||||
# perInstance, perMachine, ...
|
# perInstance, perMachine, ...
|
||||||
CLAN_SERVICE_INTERFACE = os.environ.get("CLAN_SERVICE_INTERFACE")
|
CLAN_SERVICE_INTERFACE = os.environ.get("CLAN_SERVICE_INTERFACE")
|
||||||
@@ -190,23 +181,6 @@ def module_header(module_name: str, has_inventory_feature: bool = False) -> str:
|
|||||||
return f"# {module_name}{indicator}\n\n"
|
return f"# {module_name}{indicator}\n\n"
|
||||||
|
|
||||||
|
|
||||||
def module_nix_usage(module_name: str) -> str:
|
|
||||||
return f"""## Usage via Nix
|
|
||||||
|
|
||||||
**This module can be also imported directly in your nixos configuration. Although it is recommended to use the [inventory](../../concepts/inventory.md) interface if available.**
|
|
||||||
|
|
||||||
Some modules are considered 'low-level' or 'expert modules' and are not available via the inventory interface.
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{{config, lib, inputs, ...}}: {{
|
|
||||||
imports = [ inputs.clan-core.clanModules.{module_name} ];
|
|
||||||
# ...
|
|
||||||
}}
|
|
||||||
```
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
clan_core_descr = """
|
clan_core_descr = """
|
||||||
`clan.core` is always present in a clan machine
|
`clan.core` is always present in a clan machine
|
||||||
|
|
||||||
@@ -223,68 +197,6 @@ The following options are available for this module.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def produce_clan_modules_frontmatter_docs() -> None:
|
|
||||||
if not CLAN_MODULES_FRONTMATTER_DOCS:
|
|
||||||
msg = f"Environment variables are not set correctly: $CLAN_CORE_DOCS={CLAN_CORE_DOCS}"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
if not OUT:
|
|
||||||
msg = f"Environment variables are not set correctly: $out={OUT}"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
with Path(CLAN_MODULES_FRONTMATTER_DOCS).open() as f:
|
|
||||||
options: dict[str, dict[str, Any]] = json.load(f)
|
|
||||||
|
|
||||||
# header
|
|
||||||
output = """# Frontmatter
|
|
||||||
|
|
||||||
Every clan module has a `frontmatter` section within its readme. It provides
|
|
||||||
machine readable metadata about the module.
|
|
||||||
|
|
||||||
!!! example
|
|
||||||
|
|
||||||
The used format is `TOML`
|
|
||||||
|
|
||||||
The content is separated by `---` and the frontmatter must be placed at the very top of the `README.md` file.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
---
|
|
||||||
description = "A description of the module"
|
|
||||||
categories = ["category1", "category2"]
|
|
||||||
|
|
||||||
[constraints]
|
|
||||||
roles.client.max = 10
|
|
||||||
roles.server.min = 1
|
|
||||||
---
|
|
||||||
# Readme content
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
output += """## Overview
|
|
||||||
|
|
||||||
This provides an overview of the available attributes of the `frontmatter`
|
|
||||||
within the `README.md` of a clan module.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# for option_name, info in options.items():
|
|
||||||
# if option_name == "_module.args":
|
|
||||||
# continue
|
|
||||||
# output += render_option(option_name, info)
|
|
||||||
root = options_to_tree(options, debug=True)
|
|
||||||
for option in root.suboptions:
|
|
||||||
output += options_docs_from_tree(option, init_level=2)
|
|
||||||
|
|
||||||
outfile = Path(OUT) / "clanModules/frontmatter/index.md"
|
|
||||||
outfile.parent.mkdir(
|
|
||||||
parents=True,
|
|
||||||
exist_ok=True,
|
|
||||||
)
|
|
||||||
with outfile.open("w") as of:
|
|
||||||
of.write(output)
|
|
||||||
|
|
||||||
|
|
||||||
def produce_clan_core_docs() -> None:
|
def produce_clan_core_docs() -> None:
|
||||||
if not CLAN_CORE_DOCS:
|
if not CLAN_CORE_DOCS:
|
||||||
msg = f"Environment variables are not set correctly: $CLAN_CORE_DOCS={CLAN_CORE_DOCS}"
|
msg = f"Environment variables are not set correctly: $CLAN_CORE_DOCS={CLAN_CORE_DOCS}"
|
||||||
@@ -505,154 +417,6 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
|
|||||||
of.write(output)
|
of.write(output)
|
||||||
|
|
||||||
|
|
||||||
def produce_clan_modules_docs() -> None:
|
|
||||||
if not CLAN_MODULES_VIA_NIX:
|
|
||||||
msg = f"Environment variables are not set correctly: $CLAN_MODULES_VIA_NIX={CLAN_MODULES_VIA_NIX}"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
if not CLAN_MODULES_VIA_ROLES:
|
|
||||||
msg = f"Environment variables are not set correctly: $CLAN_MODULES_VIA_ROLES={CLAN_MODULES_VIA_ROLES}"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
if not CLAN_CORE_PATH:
|
|
||||||
msg = f"Environment variables are not set correctly: $CLAN_CORE_PATH={CLAN_CORE_PATH}"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
if not OUT:
|
|
||||||
msg = f"Environment variables are not set correctly: $out={OUT}"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
modules_index = "# Modules Overview\n\n"
|
|
||||||
modules_index += clan_modules_descr
|
|
||||||
modules_index += "## Overview\n\n"
|
|
||||||
modules_index += '<div class="grid cards" markdown>\n\n'
|
|
||||||
|
|
||||||
with Path(CLAN_MODULES_VIA_ROLES).open() as f2:
|
|
||||||
role_links: dict[str, dict[str, str]] = json.load(f2)
|
|
||||||
|
|
||||||
with Path(CLAN_MODULES_VIA_NIX).open() as f:
|
|
||||||
links: dict[str, str] = json.load(f)
|
|
||||||
|
|
||||||
for module_name, options_file in links.items():
|
|
||||||
print(f"Rendering ClanModule: {module_name}")
|
|
||||||
readme_file = CLAN_CORE_PATH / "clanModules" / module_name / "README.md"
|
|
||||||
with readme_file.open() as f:
|
|
||||||
readme = f.read()
|
|
||||||
frontmatter: Frontmatter
|
|
||||||
frontmatter, readme_content = extract_frontmatter(readme, str(readme_file))
|
|
||||||
|
|
||||||
# skip if experimental feature enabled
|
|
||||||
if "experimental" in frontmatter.features:
|
|
||||||
print(f"Skipping {module_name}: Experimental feature")
|
|
||||||
continue
|
|
||||||
|
|
||||||
modules_index += build_option_card(module_name, frontmatter)
|
|
||||||
|
|
||||||
##### Print module documentation #####
|
|
||||||
|
|
||||||
# 1. Header
|
|
||||||
output = module_header(module_name, "inventory" in frontmatter.features)
|
|
||||||
|
|
||||||
# 2. Description from README.md
|
|
||||||
if frontmatter.description:
|
|
||||||
output += f"*{frontmatter.description}*\n\n"
|
|
||||||
|
|
||||||
# 2. Deprecation note if the module is deprecated
|
|
||||||
if "deprecated" in frontmatter.features:
|
|
||||||
output += f"""
|
|
||||||
!!! Warning "Deprecated"
|
|
||||||
The `{module_name}` module is deprecated.*
|
|
||||||
|
|
||||||
Use 'clanServices/{module_name}' or a similar successor instead
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
output += f"""
|
|
||||||
!!! Warning "Will be deprecated"
|
|
||||||
The `{module_name}` module might eventually be migrated to 'clanServices'*
|
|
||||||
|
|
||||||
See: [clanServices](../../guides/clanServices.md)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 3. Categories from README.md
|
|
||||||
output += "## Categories\n\n"
|
|
||||||
output += render_categories(frontmatter.categories, frontmatter.categories_info)
|
|
||||||
output += "\n---\n\n"
|
|
||||||
|
|
||||||
# 3. README.md content
|
|
||||||
output += f"{readme_content}\n"
|
|
||||||
|
|
||||||
# 4. Usage
|
|
||||||
##### Print usage via Inventory #####
|
|
||||||
|
|
||||||
# get_roles(str) -> list[str] | None
|
|
||||||
# if not isinstance(options_file, str):
|
|
||||||
roles = get_roles(CLAN_CORE_PATH / "clanModules" / module_name)
|
|
||||||
if roles:
|
|
||||||
# Render inventory usage
|
|
||||||
output += """## Usage via Inventory\n\n"""
|
|
||||||
output += render_roles(roles, module_name)
|
|
||||||
for role in roles:
|
|
||||||
role_options_file = role_links[module_name][role]
|
|
||||||
# Abort if the options file is not found
|
|
||||||
if not isinstance(role_options_file, str):
|
|
||||||
print(
|
|
||||||
f"Error: module: {module_name} in role: {role} - options file not found, Got {role_options_file}"
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
no_options = f"""### Options of `{role}` role
|
|
||||||
|
|
||||||
**The `{module_name}` `{role}` doesnt offer / require any options to be set.**
|
|
||||||
"""
|
|
||||||
|
|
||||||
heading = f"""### Options of `{role}` role
|
|
||||||
|
|
||||||
The following options are available when using the `{role}` role.
|
|
||||||
"""
|
|
||||||
output += print_options(
|
|
||||||
role_options_file,
|
|
||||||
heading,
|
|
||||||
no_options,
|
|
||||||
replace_prefix=f"clan.{module_name}",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# No roles means no inventory usage
|
|
||||||
output += """## Usage via Inventory
|
|
||||||
|
|
||||||
**This module cannot be used via the inventory interface.**
|
|
||||||
"""
|
|
||||||
|
|
||||||
##### Print usage via Nix / nixos #####
|
|
||||||
if not isinstance(options_file, str):
|
|
||||||
print(
|
|
||||||
f"Skipping {module_name}: Cannot be used via import clanModules.{module_name}"
|
|
||||||
)
|
|
||||||
output += """## Usage via Nix
|
|
||||||
|
|
||||||
**This module cannot be imported directly in your nixos configuration.**
|
|
||||||
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
output += module_nix_usage(module_name)
|
|
||||||
no_options = "** This module doesnt require any options to be set.**"
|
|
||||||
output += print_options(options_file, options_head, no_options)
|
|
||||||
|
|
||||||
outfile = Path(OUT) / f"clanModules/{module_name}.md"
|
|
||||||
outfile.parent.mkdir(
|
|
||||||
parents=True,
|
|
||||||
exist_ok=True,
|
|
||||||
)
|
|
||||||
with outfile.open("w") as of:
|
|
||||||
of.write(output)
|
|
||||||
|
|
||||||
modules_index += "</div>"
|
|
||||||
modules_index += "\n"
|
|
||||||
modules_outfile = Path(OUT) / "clanModules/index.md"
|
|
||||||
|
|
||||||
with modules_outfile.open("w") as of:
|
|
||||||
of.write(modules_index)
|
|
||||||
|
|
||||||
|
|
||||||
def build_option_card(module_name: str, frontmatter: Frontmatter) -> str:
|
def build_option_card(module_name: str, frontmatter: Frontmatter) -> str:
|
||||||
"""
|
"""
|
||||||
Build the overview index card for each reference target option.
|
Build the overview index card for each reference target option.
|
||||||
@@ -863,8 +627,4 @@ if __name__ == "__main__": #
|
|||||||
produce_clan_core_docs()
|
produce_clan_core_docs()
|
||||||
|
|
||||||
produce_clan_service_author_docs()
|
produce_clan_service_author_docs()
|
||||||
|
|
||||||
# produce_clan_modules_docs()
|
|
||||||
produce_clan_service_docs()
|
produce_clan_service_docs()
|
||||||
|
|
||||||
# produce_clan_modules_frontmatter_docs()
|
|
||||||
|
|||||||
@@ -67,7 +67,6 @@
|
|||||||
clan = {
|
clan = {
|
||||||
meta.name = "clan-core";
|
meta.name = "clan-core";
|
||||||
inventory = {
|
inventory = {
|
||||||
services = { };
|
|
||||||
machines = {
|
machines = {
|
||||||
"test-darwin-machine" = {
|
"test-darwin-machine" = {
|
||||||
machineClass = "darwin";
|
machineClass = "darwin";
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ lib.fix (
|
|||||||
evalService = clanLib.callLib ./modules/inventory/distributed-service/evalService.nix { };
|
evalService = clanLib.callLib ./modules/inventory/distributed-service/evalService.nix { };
|
||||||
# ------------------------------------
|
# ------------------------------------
|
||||||
# ClanLib functions
|
# ClanLib functions
|
||||||
evalClan = clanLib.callLib ./modules/inventory/eval-clan-modules { };
|
|
||||||
inventory = clanLib.callLib ./modules/inventory { };
|
inventory = clanLib.callLib ./modules/inventory { };
|
||||||
modules = clanLib.callLib ./modules/inventory/frontmatter { };
|
modules = clanLib.callLib ./modules/inventory/frontmatter { };
|
||||||
test = clanLib.callLib ./test { };
|
test = clanLib.callLib ./test { };
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
clanLib,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
baseModule =
|
|
||||||
{ pkgs }:
|
|
||||||
# Module
|
|
||||||
{ config, ... }:
|
|
||||||
{
|
|
||||||
imports = (import (pkgs.path + "/nixos/modules/module-list.nix"));
|
|
||||||
nixpkgs.pkgs = pkgs;
|
|
||||||
clan.core.name = "dummy";
|
|
||||||
system.stateVersion = config.system.nixos.release;
|
|
||||||
# Set this to work around a bug where `clan.core.settings.machine.name`
|
|
||||||
# is forced due to `networking.interfaces` being forced
|
|
||||||
# somewhere in the nixpkgs options
|
|
||||||
facter.detected.dhcp.enable = lib.mkForce false;
|
|
||||||
};
|
|
||||||
|
|
||||||
# This function takes a list of module names and evaluates them
|
|
||||||
# [ module ] -> { config, options, ... }
|
|
||||||
evalClanModulesLegacy =
|
|
||||||
{
|
|
||||||
modules,
|
|
||||||
pkgs,
|
|
||||||
clan-core,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
evaled = lib.evalModules {
|
|
||||||
class = "nixos";
|
|
||||||
modules = [
|
|
||||||
(baseModule { inherit pkgs; })
|
|
||||||
{
|
|
||||||
clan.core.settings.directory = clan-core;
|
|
||||||
}
|
|
||||||
clan-core.nixosModules.clanCore
|
|
||||||
]
|
|
||||||
++ modules;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
# lib.warn ''
|
|
||||||
# doesn't respect role specific interfaces.
|
|
||||||
|
|
||||||
# The following {module}/default.nix file trying to be imported.
|
|
||||||
|
|
||||||
# Modules: ${builtins.toJSON modulenames}
|
|
||||||
|
|
||||||
# This might result in incomplete or incorrect interfaces.
|
|
||||||
|
|
||||||
# FIX: Use evalClanModuleWithRole instead.
|
|
||||||
# ''
|
|
||||||
evaled;
|
|
||||||
|
|
||||||
/*
|
|
||||||
This function takes a list of module names and evaluates them
|
|
||||||
Returns a set of interfaces as described below:
|
|
||||||
|
|
||||||
Fn :: { ${moduleName} = Module; } -> {
|
|
||||||
${moduleName} :: {
|
|
||||||
${roleName}: JSONSchema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
evalClanModulesWithRoles =
|
|
||||||
{
|
|
||||||
allModules,
|
|
||||||
clan-core,
|
|
||||||
pkgs,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
res = builtins.mapAttrs (
|
|
||||||
moduleName: module:
|
|
||||||
let
|
|
||||||
frontmatter = clanLib.modules.getFrontmatter allModules.${moduleName} moduleName;
|
|
||||||
roles =
|
|
||||||
if builtins.elem "inventory" frontmatter.features or [ ] then
|
|
||||||
assert lib.isPath module;
|
|
||||||
clan-core.clanLib.modules.getRoles "Documentation: inventory.modules" allModules moduleName
|
|
||||||
else
|
|
||||||
[ ];
|
|
||||||
in
|
|
||||||
lib.listToAttrs (
|
|
||||||
lib.map (role: {
|
|
||||||
name = role;
|
|
||||||
value =
|
|
||||||
(lib.evalModules {
|
|
||||||
class = "nixos";
|
|
||||||
modules = [
|
|
||||||
(baseModule { inherit pkgs; })
|
|
||||||
clan-core.nixosModules.clanCore
|
|
||||||
{
|
|
||||||
clan.core.settings.directory = clan-core;
|
|
||||||
}
|
|
||||||
# Role interface
|
|
||||||
(module + "/roles/${role}.nix")
|
|
||||||
];
|
|
||||||
}).options.clan.${moduleName} or { };
|
|
||||||
}) roles
|
|
||||||
)
|
|
||||||
) allModules;
|
|
||||||
in
|
|
||||||
res;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
evalClanModules = evalClanModulesLegacy;
|
|
||||||
inherit evalClanModulesWithRoles;
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
{
|
{
|
||||||
self,
|
self,
|
||||||
inputs,
|
|
||||||
options,
|
options,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./distributed-service/flake-module.nix
|
./distributed-service/flake-module.nix
|
||||||
@@ -15,16 +11,13 @@ in
|
|||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
config,
|
|
||||||
system,
|
|
||||||
self',
|
self',
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
devShells.inventory-schema = pkgs.mkShell {
|
devShells.inventory-schema = pkgs.mkShell {
|
||||||
name = "clan-inventory-schema";
|
name = "clan-inventory-schema";
|
||||||
inputsFrom = with config.checks; [
|
inputsFrom = [
|
||||||
eval-lib-inventory
|
|
||||||
self'.devShells.default
|
self'.devShells.default
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -51,41 +44,5 @@ in
|
|||||||
warningsAreErrors = true;
|
warningsAreErrors = true;
|
||||||
transformOptions = self.clanLib.docs.stripStorePathsFromDeclarations;
|
transformOptions = self.clanLib.docs.stripStorePathsFromDeclarations;
|
||||||
}).optionsJSON;
|
}).optionsJSON;
|
||||||
|
|
||||||
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
|
|
||||||
legacyPackages.evalTests-inventory = import ./tests {
|
|
||||||
inherit lib;
|
|
||||||
clan-core = self;
|
|
||||||
inherit (self) clanLib;
|
|
||||||
inherit (self.inputs) nix-darwin;
|
|
||||||
};
|
|
||||||
|
|
||||||
checks = {
|
|
||||||
eval-lib-inventory = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
|
|
||||||
export HOME="$(realpath .)"
|
|
||||||
export NIX_ABORT_ON_WARN=1
|
|
||||||
nix-unit --eval-store "$HOME" \
|
|
||||||
--extra-experimental-features flakes \
|
|
||||||
--show-trace \
|
|
||||||
${inputOverrides} \
|
|
||||||
--flake ${
|
|
||||||
lib.fileset.toSource {
|
|
||||||
root = ../../..;
|
|
||||||
fileset = lib.fileset.unions [
|
|
||||||
../../../flake.nix
|
|
||||||
../../../flake.lock
|
|
||||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../..)
|
|
||||||
../../../flakeModules
|
|
||||||
../../../lib
|
|
||||||
../../../nixosModules/clanCore
|
|
||||||
../../../machines
|
|
||||||
../../../inventory.json
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}#legacyPackages.${system}.evalTests-inventory
|
|
||||||
|
|
||||||
touch $out
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,51 +3,6 @@ let
|
|||||||
# Trim the .nix extension from a filename
|
# Trim the .nix extension from a filename
|
||||||
trimExtension = name: builtins.substring 0 (builtins.stringLength name - 4) name;
|
trimExtension = name: builtins.substring 0 (builtins.stringLength name - 4) name;
|
||||||
|
|
||||||
jsonWithoutHeader = clanLib.jsonschema {
|
|
||||||
includeDefaults = true;
|
|
||||||
header = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
getModulesSchema =
|
|
||||||
{
|
|
||||||
modules,
|
|
||||||
clan-core,
|
|
||||||
pkgs,
|
|
||||||
}:
|
|
||||||
lib.mapAttrs
|
|
||||||
(
|
|
||||||
_moduleName: rolesOptions:
|
|
||||||
lib.mapAttrs (_roleName: options: jsonWithoutHeader.parseOptions options { }) rolesOptions
|
|
||||||
)
|
|
||||||
(
|
|
||||||
clanLib.evalClan.evalClanModulesWithRoles {
|
|
||||||
allModules = modules;
|
|
||||||
inherit pkgs clan-core;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
evalFrontmatter =
|
|
||||||
{
|
|
||||||
moduleName,
|
|
||||||
instanceName,
|
|
||||||
resolvedRoles,
|
|
||||||
allModules,
|
|
||||||
}:
|
|
||||||
lib.evalModules {
|
|
||||||
modules = [
|
|
||||||
(getFrontmatter allModules.${moduleName} moduleName)
|
|
||||||
./interface.nix
|
|
||||||
{
|
|
||||||
constraints.imports = [
|
|
||||||
(lib.modules.importApply ../constraints {
|
|
||||||
inherit moduleName resolvedRoles instanceName;
|
|
||||||
allRoles = getRoles "inventory.modules" allModules moduleName;
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# For Documentation purposes only
|
# For Documentation purposes only
|
||||||
frontmatterOptions =
|
frontmatterOptions =
|
||||||
(lib.evalModules {
|
(lib.evalModules {
|
||||||
@@ -119,17 +74,12 @@ let
|
|||||||
builtins.readDir (checkedPath)
|
builtins.readDir (checkedPath)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
checkConstraints = args: (evalFrontmatter args).config.constraints.assertions;
|
|
||||||
getFrontmatter = _modulepath: _modulename: "clanModules are removed!";
|
getFrontmatter = _modulepath: _modulename: "clanModules are removed!";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit
|
inherit
|
||||||
frontmatterOptions
|
frontmatterOptions
|
||||||
getModulesSchema
|
|
||||||
getFrontmatter
|
getFrontmatter
|
||||||
|
|
||||||
checkConstraints
|
|
||||||
getRoles
|
getRoles
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,14 @@
|
|||||||
{
|
{
|
||||||
self,
|
self,
|
||||||
self',
|
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
flakeOptions,
|
flakeOptions,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
|
||||||
modulesSchema = self.clanLib.modules.getModulesSchema {
|
|
||||||
modules = self.clanModules;
|
|
||||||
inherit pkgs;
|
|
||||||
clan-core = self;
|
|
||||||
};
|
|
||||||
|
|
||||||
jsonLib = self.clanLib.jsonschema { inherit includeDefaults; };
|
jsonLib = self.clanLib.jsonschema { inherit includeDefaults; };
|
||||||
includeDefaults = true;
|
includeDefaults = true;
|
||||||
|
|
||||||
frontMatterSchema = jsonLib.parseOptions self.clanLib.modules.frontmatterOptions { };
|
|
||||||
|
|
||||||
inventorySchema = jsonLib.parseModule ({
|
|
||||||
imports = [ ../../inventoryClass/interface.nix ];
|
|
||||||
_module.args = { inherit (self) clanLib; };
|
|
||||||
});
|
|
||||||
|
|
||||||
opts = (flakeOptions.flake.type.getSubOptions [ "flake" ]);
|
opts = (flakeOptions.flake.type.getSubOptions [ "flake" ]);
|
||||||
clanOpts = opts.clan.type.getSubOptions [ "clan" ];
|
clanOpts = opts.clan.type.getSubOptions [ "clan" ];
|
||||||
include = [
|
include = [
|
||||||
@@ -38,13 +23,6 @@ let
|
|||||||
];
|
];
|
||||||
clanSchema = jsonLib.parseOptions (lib.filterAttrs (n: _v: lib.elem n include) clanOpts) { };
|
clanSchema = jsonLib.parseOptions (lib.filterAttrs (n: _v: lib.elem n include) clanOpts) { };
|
||||||
|
|
||||||
renderSchema = pkgs.writers.writePython3Bin "render-schema" {
|
|
||||||
flakeIgnore = [
|
|
||||||
"F401"
|
|
||||||
"E501"
|
|
||||||
];
|
|
||||||
} ./render_schema.py;
|
|
||||||
|
|
||||||
clan-schema-abstract = pkgs.stdenv.mkDerivation {
|
clan-schema-abstract = pkgs.stdenv.mkDerivation {
|
||||||
name = "clan-schema-files";
|
name = "clan-schema-files";
|
||||||
buildInputs = [ pkgs.cue ];
|
buildInputs = [ pkgs.cue ];
|
||||||
@@ -63,29 +41,7 @@ in
|
|||||||
{
|
{
|
||||||
inherit
|
inherit
|
||||||
flakeOptions
|
flakeOptions
|
||||||
frontMatterSchema
|
|
||||||
clanSchema
|
clanSchema
|
||||||
inventorySchema
|
|
||||||
modulesSchema
|
|
||||||
renderSchema
|
|
||||||
clan-schema-abstract
|
clan-schema-abstract
|
||||||
;
|
;
|
||||||
|
|
||||||
# Inventory schema, with the modules schema added per role
|
|
||||||
inventory =
|
|
||||||
pkgs.runCommand "rendered"
|
|
||||||
{
|
|
||||||
buildInputs = [
|
|
||||||
pkgs.python3
|
|
||||||
self'.packages.clan-cli
|
|
||||||
];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
export INVENTORY_SCHEMA_PATH=${builtins.toFile "inventory-schema.json" (builtins.toJSON inventorySchema)}
|
|
||||||
export MODULES_SCHEMA_PATH=${builtins.toFile "modules-schema.json" (builtins.toJSON modulesSchema)}
|
|
||||||
|
|
||||||
mkdir $out
|
|
||||||
# The python script will place the schemas in the output directory
|
|
||||||
exec python3 ${renderSchema}/bin/render-schema
|
|
||||||
'';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
"""
|
|
||||||
Python script to join the abstract inventory schema, with the concrete clan modules
|
|
||||||
Inventory has slots which are 'Any' type.
|
|
||||||
We dont want to evaluate the clanModules interface in nix, when evaluating the inventory
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from clan_lib.errors import ClanError
|
|
||||||
|
|
||||||
# Get environment variables
|
|
||||||
INVENTORY_SCHEMA_PATH = Path(os.environ["INVENTORY_SCHEMA_PATH"])
|
|
||||||
|
|
||||||
# { [moduleName] :: { [roleName] :: SCHEMA }}
|
|
||||||
MODULES_SCHEMA_PATH = Path(os.environ["MODULES_SCHEMA_PATH"])
|
|
||||||
|
|
||||||
OUT = os.environ.get("out")
|
|
||||||
|
|
||||||
if not INVENTORY_SCHEMA_PATH:
|
|
||||||
msg = f"Environment variables are not set correctly: INVENTORY_SCHEMA_PATH={INVENTORY_SCHEMA_PATH}."
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
if not MODULES_SCHEMA_PATH:
|
|
||||||
msg = f"Environment variables are not set correctly: MODULES_SCHEMA_PATH={MODULES_SCHEMA_PATH}."
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
if not OUT:
|
|
||||||
msg = f"Environment variables are not set correctly: OUT={OUT}."
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def service_roles_to_schema(
|
|
||||||
schema: dict[str, Any],
|
|
||||||
service_name: str,
|
|
||||||
roles: list[str],
|
|
||||||
roles_schemas: dict[str, dict[str, Any]],
|
|
||||||
# Original service properties: {'config': Schema, 'machines': Schema, 'meta': Schema, 'extraModules': Schema, ...?}
|
|
||||||
orig: dict[str, Any],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Add roles to the service schema
|
|
||||||
"""
|
|
||||||
# collect all the roles for the service, to form a type union
|
|
||||||
all_roles_schema: list[dict[str, Any]] = []
|
|
||||||
for role_name, role_schema in roles_schemas.items():
|
|
||||||
role_schema["title"] = f"{module_name}-config-role-{role_name}"
|
|
||||||
all_roles_schema.append(role_schema)
|
|
||||||
|
|
||||||
role_schema = {}
|
|
||||||
for role in roles:
|
|
||||||
role_schema[role] = {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": False,
|
|
||||||
"properties": {
|
|
||||||
**orig["roles"]["additionalProperties"]["properties"],
|
|
||||||
"config": {
|
|
||||||
**roles_schemas.get(role, {}),
|
|
||||||
"title": f"{service_name}-config-role-{role}",
|
|
||||||
"type": "object",
|
|
||||||
"default": {},
|
|
||||||
"additionalProperties": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
machines_schema = {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
**orig["machines"]["additionalProperties"]["properties"],
|
|
||||||
"config": {
|
|
||||||
"title": f"{service_name}-config",
|
|
||||||
"oneOf": all_roles_schema,
|
|
||||||
"type": "object",
|
|
||||||
"default": {},
|
|
||||||
"additionalProperties": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
services["properties"][service_name] = {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": False,
|
|
||||||
"properties": {
|
|
||||||
# Original inventory schema
|
|
||||||
**orig,
|
|
||||||
# Inject the roles schemas
|
|
||||||
"roles": {
|
|
||||||
"title": f"{service_name}-roles",
|
|
||||||
"type": "object",
|
|
||||||
"properties": role_schema,
|
|
||||||
"additionalProperties": False,
|
|
||||||
},
|
|
||||||
"machines": machines_schema,
|
|
||||||
"config": {
|
|
||||||
"title": f"{service_name}-config",
|
|
||||||
"oneOf": all_roles_schema,
|
|
||||||
"type": "object",
|
|
||||||
"default": {},
|
|
||||||
"additionalProperties": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Joining inventory schema with modules schema")
|
|
||||||
print(f"Inventory schema path: {INVENTORY_SCHEMA_PATH}")
|
|
||||||
print(f"Modules schema path: {MODULES_SCHEMA_PATH}")
|
|
||||||
|
|
||||||
modules_schema = {}
|
|
||||||
with Path.open(MODULES_SCHEMA_PATH) as f:
|
|
||||||
modules_schema = json.load(f)
|
|
||||||
|
|
||||||
inventory_schema = {}
|
|
||||||
with Path.open(INVENTORY_SCHEMA_PATH) as f:
|
|
||||||
inventory_schema = json.load(f)
|
|
||||||
|
|
||||||
services = inventory_schema["properties"]["services"]
|
|
||||||
original_service_props = services["additionalProperties"]["additionalProperties"][
|
|
||||||
"properties"
|
|
||||||
].copy()
|
|
||||||
# Init the outer services schema
|
|
||||||
# Properties (service names) will be filled in the next step
|
|
||||||
services = {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
# Service names
|
|
||||||
},
|
|
||||||
"additionalProperties": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
for module_name, roles_schemas in modules_schema.items():
|
|
||||||
# Add the roles schemas to the service schema
|
|
||||||
roles = list(roles_schemas.keys())
|
|
||||||
if roles:
|
|
||||||
services = service_roles_to_schema(
|
|
||||||
services,
|
|
||||||
module_name,
|
|
||||||
roles,
|
|
||||||
roles_schemas,
|
|
||||||
original_service_props,
|
|
||||||
)
|
|
||||||
|
|
||||||
inventory_schema["properties"]["services"] = services
|
|
||||||
|
|
||||||
outpath = Path(OUT)
|
|
||||||
with (outpath / "schema.json").open("w") as f:
|
|
||||||
json.dump(inventory_schema, f, indent=2)
|
|
||||||
|
|
||||||
with (outpath / "modules_schemas.json").open("w") as f:
|
|
||||||
json.dump(modules_schema, f, indent=2)
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
{
|
|
||||||
clan-core,
|
|
||||||
nix-darwin,
|
|
||||||
lib,
|
|
||||||
clanLib,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
# TODO: Unify these tests with clan tests
|
|
||||||
clan =
|
|
||||||
m:
|
|
||||||
lib.evalModules {
|
|
||||||
specialArgs = { inherit clan-core nix-darwin clanLib; };
|
|
||||||
modules = [
|
|
||||||
clan-core.modules.clan.default
|
|
||||||
{
|
|
||||||
self = { };
|
|
||||||
}
|
|
||||||
m
|
|
||||||
];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
test_inventory_a =
|
|
||||||
let
|
|
||||||
eval = clan {
|
|
||||||
inventory = {
|
|
||||||
machines = {
|
|
||||||
A = { };
|
|
||||||
};
|
|
||||||
services = {
|
|
||||||
legacyModule = { };
|
|
||||||
};
|
|
||||||
modules = {
|
|
||||||
legacyModule = ./legacyModule;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
directory = ./.;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit eval;
|
|
||||||
expr = {
|
|
||||||
legacyModule = lib.filterAttrs (
|
|
||||||
name: _: name == "isClanModule"
|
|
||||||
) eval.config.clanInternals.inventoryClass.machines.A.compiledServices.legacyModule;
|
|
||||||
};
|
|
||||||
expected = {
|
|
||||||
legacyModule = {
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
test_inventory_empty =
|
|
||||||
let
|
|
||||||
eval = clan {
|
|
||||||
inventory = { };
|
|
||||||
directory = ./.;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
# Empty inventory should return an empty module
|
|
||||||
expr = eval.config.clanInternals.inventoryClass.machines;
|
|
||||||
expected = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
test_inventory_module_doesnt_exist =
|
|
||||||
let
|
|
||||||
eval = clan {
|
|
||||||
directory = ./.;
|
|
||||||
inventory = {
|
|
||||||
services = {
|
|
||||||
fanatasy.instance_1 = {
|
|
||||||
roles.default.machines = [ "machine_1" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
machines = {
|
|
||||||
"machine_1" = { };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit eval;
|
|
||||||
expr = eval.config.clanInternals.inventoryClass.machines.machine_1.machineImports;
|
|
||||||
expectedError = {
|
|
||||||
type = "ThrownError";
|
|
||||||
msg = "ClanModule not found*";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
features = [ "inventory" ]
|
|
||||||
---
|
|
||||||
Description
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
clan-core,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
# Just some random stuff
|
|
||||||
options.test = lib.mapAttrs clan-core;
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# Integrity validation of the inventory
|
|
||||||
{ config, lib, ... }:
|
|
||||||
{
|
|
||||||
# Assertion must be of type
|
|
||||||
# { assertion :: bool, message :: string, severity :: "error" | "warning" }
|
|
||||||
imports = [
|
|
||||||
# Check that each machine used in a service is defined in the top-level machines
|
|
||||||
{
|
|
||||||
assertions = lib.foldlAttrs (
|
|
||||||
ass1: serviceName: c:
|
|
||||||
ass1
|
|
||||||
++ lib.foldlAttrs (
|
|
||||||
ass2: instanceName: instanceConfig:
|
|
||||||
let
|
|
||||||
topLevelMachines = lib.attrNames config.machines;
|
|
||||||
# All machines must be defined in the top-level machines
|
|
||||||
assertions = lib.foldlAttrs (
|
|
||||||
assertions: roleName: role:
|
|
||||||
assertions
|
|
||||||
++ builtins.filter (a: !a.assertion) (
|
|
||||||
builtins.map (m: {
|
|
||||||
assertion = builtins.elem m topLevelMachines;
|
|
||||||
message = ''
|
|
||||||
Machine '${m}' is not defined in the inventory. This might still work, if the machine is defined via nix.
|
|
||||||
|
|
||||||
Defined in service: '${serviceName}' instance: '${instanceName}' role: '${roleName}'.
|
|
||||||
|
|
||||||
Inventory machines:
|
|
||||||
${builtins.concatStringsSep "\n" (map (n: "'${n}'") topLevelMachines)}
|
|
||||||
'';
|
|
||||||
severity = "warning";
|
|
||||||
}) role.machines
|
|
||||||
)
|
|
||||||
) [ ] instanceConfig.roles;
|
|
||||||
in
|
|
||||||
ass2 ++ assertions
|
|
||||||
) [ ] c
|
|
||||||
) [ ] config.services;
|
|
||||||
}
|
|
||||||
# Check that each tag used in a role is defined in at least one machines tags
|
|
||||||
{
|
|
||||||
assertions = lib.foldlAttrs (
|
|
||||||
ass1: serviceName: c:
|
|
||||||
ass1
|
|
||||||
++ lib.foldlAttrs (
|
|
||||||
ass2: instanceName: instanceConfig:
|
|
||||||
let
|
|
||||||
allTags = lib.foldlAttrs (
|
|
||||||
tags: _machineName: machine:
|
|
||||||
tags ++ machine.tags
|
|
||||||
) [ ] config.machines;
|
|
||||||
# All machines must be defined in the top-level machines
|
|
||||||
assertions = lib.foldlAttrs (
|
|
||||||
assertions: roleName: role:
|
|
||||||
assertions
|
|
||||||
++ builtins.filter (a: !a.assertion) (
|
|
||||||
builtins.map (m: {
|
|
||||||
assertion = builtins.elem m allTags;
|
|
||||||
message = ''
|
|
||||||
Tag '${m}' is not defined in the inventory.
|
|
||||||
|
|
||||||
Defined in service: '${serviceName}' instance: '${instanceName}' role: '${roleName}'.
|
|
||||||
|
|
||||||
Available tags:
|
|
||||||
${builtins.concatStringsSep "\n" (map (n: "'${n}'") allTags)}
|
|
||||||
'';
|
|
||||||
severity = "error";
|
|
||||||
}) role.tags
|
|
||||||
)
|
|
||||||
) [ ] instanceConfig.roles;
|
|
||||||
in
|
|
||||||
ass2 ++ assertions
|
|
||||||
) [ ] c
|
|
||||||
) [ ] config.services;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,268 +1,5 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
clanLib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
inherit (config) inventory directory;
|
|
||||||
resolveTags =
|
|
||||||
# Inventory, { machines :: [string], tags :: [string] }
|
|
||||||
{
|
|
||||||
serviceName,
|
|
||||||
instanceName,
|
|
||||||
roleName,
|
|
||||||
inventory,
|
|
||||||
members,
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
machines =
|
|
||||||
members.machines or [ ]
|
|
||||||
++ (builtins.foldl' (
|
|
||||||
acc: tag:
|
|
||||||
let
|
|
||||||
# For error printing
|
|
||||||
availableTags = lib.foldlAttrs (
|
|
||||||
acc: _: v:
|
|
||||||
v.tags or [ ] ++ acc
|
|
||||||
) [ ] (inventory.machines);
|
|
||||||
|
|
||||||
tagMembers = builtins.attrNames (
|
|
||||||
lib.filterAttrs (_n: v: builtins.elem tag v.tags or [ ]) inventory.machines
|
|
||||||
);
|
|
||||||
in
|
|
||||||
if tagMembers == [ ] then
|
|
||||||
lib.warn ''
|
|
||||||
inventory.services.${serviceName}.${instanceName}: - ${roleName} tags: no machine with tag '${tag}' found.
|
|
||||||
Available tags: ${builtins.toJSON (lib.unique availableTags)}
|
|
||||||
'' [ ]
|
|
||||||
else
|
|
||||||
acc ++ tagMembers
|
|
||||||
) [ ] members.tags or [ ]);
|
|
||||||
};
|
|
||||||
|
|
||||||
checkService =
|
|
||||||
modulepath: serviceName:
|
|
||||||
builtins.elem "inventory" (clanLib.modules.getFrontmatter modulepath serviceName).features or [ ];
|
|
||||||
|
|
||||||
compileMachine =
|
|
||||||
{ machineConfig }:
|
|
||||||
{
|
|
||||||
machineImports = [
|
|
||||||
(lib.optionalAttrs (machineConfig.deploy.targetHost or null != null) {
|
|
||||||
config.clan.core.networking.targetHost = lib.mkForce machineConfig.deploy.targetHost;
|
|
||||||
})
|
|
||||||
(lib.optionalAttrs (machineConfig.deploy.buildHost or null != null) {
|
|
||||||
config.clan.core.networking.buildHost = lib.mkForce machineConfig.deploy.buildHost;
|
|
||||||
})
|
|
||||||
];
|
|
||||||
assertions = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
resolveImports =
|
|
||||||
{
|
|
||||||
supportedRoles,
|
|
||||||
resolvedRolesPerInstance,
|
|
||||||
serviceConfigs,
|
|
||||||
serviceName,
|
|
||||||
machineName,
|
|
||||||
getRoleFile,
|
|
||||||
}:
|
|
||||||
(lib.foldlAttrs (
|
|
||||||
# : [ Modules ] -> String -> ServiceConfig -> [ Modules ]
|
|
||||||
acc2: instanceName: serviceConfig:
|
|
||||||
let
|
|
||||||
resolvedRoles = resolvedRolesPerInstance.${instanceName};
|
|
||||||
|
|
||||||
isInService = builtins.any (members: builtins.elem machineName members.machines) (
|
|
||||||
builtins.attrValues resolvedRoles
|
|
||||||
);
|
|
||||||
|
|
||||||
# all roles where the machine is present
|
|
||||||
machineRoles = builtins.attrNames (
|
|
||||||
lib.filterAttrs (_role: roleConfig: builtins.elem machineName roleConfig.machines) resolvedRoles
|
|
||||||
);
|
|
||||||
|
|
||||||
machineServiceConfig = (serviceConfig.machines.${machineName} or { }).config or { };
|
|
||||||
globalConfig = serviceConfig.config or { };
|
|
||||||
|
|
||||||
globalExtraModules = serviceConfig.extraModules or [ ];
|
|
||||||
machineExtraModules = serviceConfig.machines.${machineName}.extraModules or [ ];
|
|
||||||
roleServiceExtraModules = builtins.foldl' (
|
|
||||||
acc: role: acc ++ serviceConfig.roles.${role}.extraModules or [ ]
|
|
||||||
) [ ] machineRoles;
|
|
||||||
|
|
||||||
# TODO: maybe optimize this don't lookup the role in inverse roles. Imports are not lazy
|
|
||||||
roleModules = builtins.map (
|
|
||||||
role:
|
|
||||||
if builtins.elem role supportedRoles && inventory.modules ? ${serviceName} then
|
|
||||||
getRoleFile role
|
|
||||||
else
|
|
||||||
throw "Module ${serviceName} doesn't have role: '${role}'. Role: ${
|
|
||||||
inventory.modules.${serviceName}
|
|
||||||
}/roles/${role}.nix not found."
|
|
||||||
) machineRoles;
|
|
||||||
|
|
||||||
roleServiceConfigs = builtins.filter (m: m != { }) (
|
|
||||||
builtins.map (role: serviceConfig.roles.${role}.config or { }) machineRoles
|
|
||||||
);
|
|
||||||
|
|
||||||
extraModules = map (s: if builtins.typeOf s == "string" then "${directory}/${s}" else s) (
|
|
||||||
globalExtraModules ++ machineExtraModules ++ roleServiceExtraModules
|
|
||||||
);
|
|
||||||
|
|
||||||
features =
|
|
||||||
(clanLib.modules.getFrontmatter inventory.modules.${serviceName} serviceName).features or [ ];
|
|
||||||
deprecationWarning = lib.optionalAttrs (builtins.elem "deprecated" features) {
|
|
||||||
warnings = [
|
|
||||||
''
|
|
||||||
The '${serviceName}' module has been migrated from `inventory.services` to `inventory.instances`
|
|
||||||
See https://docs.clan.lol/guides/clanServices/ for usage.
|
|
||||||
''
|
|
||||||
];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
if !(serviceConfig.enabled or true) then
|
|
||||||
acc2
|
|
||||||
else if isInService then
|
|
||||||
acc2
|
|
||||||
++ [
|
|
||||||
deprecationWarning
|
|
||||||
{
|
|
||||||
imports = roleModules ++ extraModules;
|
|
||||||
clan.inventory.services.${serviceName}.${instanceName} = {
|
|
||||||
roles = resolvedRoles;
|
|
||||||
# TODO: Add inverseRoles to the service config if needed
|
|
||||||
# inherit inverseRoles;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(lib.optionalAttrs (globalConfig != { } || machineServiceConfig != { } || roleServiceConfigs != [ ])
|
|
||||||
{
|
|
||||||
clan.${serviceName} = lib.mkMerge (
|
|
||||||
[
|
|
||||||
globalConfig
|
|
||||||
machineServiceConfig
|
|
||||||
]
|
|
||||||
++ roleServiceConfigs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
else
|
|
||||||
acc2
|
|
||||||
) [ ] (serviceConfigs));
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./interface.nix
|
./interface.nix
|
||||||
];
|
];
|
||||||
config = {
|
|
||||||
machines = builtins.mapAttrs (
|
|
||||||
machineName: machineConfig: m:
|
|
||||||
let
|
|
||||||
compiledServices = lib.mapAttrs (
|
|
||||||
_: serviceConfigs:
|
|
||||||
(
|
|
||||||
{ config, ... }:
|
|
||||||
let
|
|
||||||
serviceName = config.serviceName;
|
|
||||||
|
|
||||||
getRoleFile = role: builtins.seq role inventory.modules.${serviceName} + "/roles/${role}.nix";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
_file = "inventory/builder.nix";
|
|
||||||
_module.args = {
|
|
||||||
inherit
|
|
||||||
resolveTags
|
|
||||||
inventory
|
|
||||||
clanLib
|
|
||||||
machineName
|
|
||||||
serviceConfigs
|
|
||||||
;
|
|
||||||
};
|
|
||||||
imports = [
|
|
||||||
./roles.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
machineImports = resolveImports {
|
|
||||||
supportedRoles = config.supportedRoles;
|
|
||||||
resolvedRolesPerInstance = config.resolvedRolesPerInstance;
|
|
||||||
inherit
|
|
||||||
serviceConfigs
|
|
||||||
serviceName
|
|
||||||
machineName
|
|
||||||
getRoleFile
|
|
||||||
;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Assertions
|
|
||||||
assertions = {
|
|
||||||
"checkservice.${serviceName}" = {
|
|
||||||
assertion = checkService inventory.modules.${serviceName} serviceName;
|
|
||||||
message = ''
|
|
||||||
Service ${serviceName} cannot be used in inventory. It does not declare the 'inventory' feature.
|
|
||||||
|
|
||||||
To allow it add the following to the beginning of the README.md of the module:
|
|
||||||
|
|
||||||
---
|
|
||||||
...
|
|
||||||
|
|
||||||
features = [ "inventory" ]
|
|
||||||
---
|
|
||||||
|
|
||||||
Also make sure to test the module with the 'inventory' feature enabled.
|
|
||||||
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) (config.inventory.services or { });
|
|
||||||
|
|
||||||
compiledMachine = compileMachine {
|
|
||||||
inherit
|
|
||||||
machineConfig
|
|
||||||
;
|
|
||||||
};
|
|
||||||
|
|
||||||
machineImports = (
|
|
||||||
compiledMachine.machineImports
|
|
||||||
++ builtins.foldl' (
|
|
||||||
acc: service:
|
|
||||||
let
|
|
||||||
failedAssertions = (lib.filterAttrs (_: v: !v.assertion) service.assertions);
|
|
||||||
failedAssertionsImports =
|
|
||||||
if failedAssertions != { } then
|
|
||||||
[
|
|
||||||
{
|
|
||||||
clan.inventory.assertions = failedAssertions;
|
|
||||||
}
|
|
||||||
]
|
|
||||||
else
|
|
||||||
[
|
|
||||||
{
|
|
||||||
clan.inventory.assertions = {
|
|
||||||
"alive.assertion.inventory" = {
|
|
||||||
assertion = true;
|
|
||||||
message = ''
|
|
||||||
No failed assertions found for machine ${machineName}. This will never be displayed.
|
|
||||||
It is here for testing purposes.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
in
|
|
||||||
acc
|
|
||||||
++ service.machineImports
|
|
||||||
# Import failed assertions
|
|
||||||
++ failedAssertionsImports
|
|
||||||
) [ ] (builtins.attrValues m.config.compiledServices)
|
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit machineImports compiledServices compiledMachine;
|
|
||||||
}
|
|
||||||
) (inventory.machines or { });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,76 +16,13 @@ in
|
|||||||
type = types.raw;
|
type = types.raw;
|
||||||
};
|
};
|
||||||
machines = mkOption {
|
machines = mkOption {
|
||||||
type = types.attrsOf (
|
type = types.attrsOf (submodule ({
|
||||||
submodule (
|
options = {
|
||||||
{ name, ... }:
|
machineImports = mkOption {
|
||||||
let
|
type = types.listOf types.raw;
|
||||||
machineName = name;
|
};
|
||||||
in
|
};
|
||||||
{
|
}));
|
||||||
options = {
|
|
||||||
compiledMachine = mkOption {
|
|
||||||
type = types.raw;
|
|
||||||
};
|
|
||||||
compiledServices = mkOption {
|
|
||||||
# type = types.attrsOf;
|
|
||||||
type = types.attrsOf (
|
|
||||||
types.submoduleWith {
|
|
||||||
modules = [
|
|
||||||
(
|
|
||||||
{ name, ... }:
|
|
||||||
let
|
|
||||||
serviceName = name;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
machineName = mkOption {
|
|
||||||
default = machineName;
|
|
||||||
readOnly = true;
|
|
||||||
};
|
|
||||||
serviceName = mkOption {
|
|
||||||
default = serviceName;
|
|
||||||
readOnly = true;
|
|
||||||
};
|
|
||||||
# Outputs
|
|
||||||
machineImports = mkOption {
|
|
||||||
type = types.listOf types.raw;
|
|
||||||
};
|
|
||||||
supportedRoles = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
};
|
|
||||||
matchedRoles = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
};
|
|
||||||
machinesRoles = mkOption {
|
|
||||||
type = types.attrsOf (types.listOf types.str);
|
|
||||||
};
|
|
||||||
resolvedRolesPerInstance = mkOption {
|
|
||||||
type = types.attrsOf (
|
|
||||||
types.attrsOf (submodule {
|
|
||||||
options.machines = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
assertions = mkOption {
|
|
||||||
type = types.attrsOf types.raw;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
machineImports = mkOption {
|
|
||||||
type = types.listOf types.raw;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
resolveTags,
|
|
||||||
inventory,
|
|
||||||
clanLib,
|
|
||||||
machineName,
|
|
||||||
serviceConfigs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
serviceName = config.serviceName;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
# Roles resolution
|
|
||||||
# : List String
|
|
||||||
supportedRoles = clanLib.modules.getRoles "inventory.modules" inventory.modules serviceName;
|
|
||||||
matchedRoles = builtins.attrNames (
|
|
||||||
lib.filterAttrs (_: ms: builtins.elem machineName ms) config.machinesRoles
|
|
||||||
);
|
|
||||||
resolvedRolesPerInstance = lib.mapAttrs (
|
|
||||||
instanceName: instanceConfig:
|
|
||||||
let
|
|
||||||
resolvedRoles = lib.genAttrs config.supportedRoles (
|
|
||||||
roleName:
|
|
||||||
resolveTags {
|
|
||||||
members = instanceConfig.roles.${roleName} or { };
|
|
||||||
inherit
|
|
||||||
instanceName
|
|
||||||
serviceName
|
|
||||||
roleName
|
|
||||||
inventory
|
|
||||||
;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
usedRoles = builtins.attrNames instanceConfig.roles;
|
|
||||||
unmatchedRoles = builtins.filter (role: !builtins.elem role config.supportedRoles) usedRoles;
|
|
||||||
in
|
|
||||||
if unmatchedRoles != [ ] then
|
|
||||||
throw ''
|
|
||||||
Roles ${builtins.toJSON unmatchedRoles} are not defined in the service ${serviceName}.
|
|
||||||
Instance: '${instanceName}'
|
|
||||||
Please use one of available roles: ${builtins.toJSON config.supportedRoles}
|
|
||||||
''
|
|
||||||
else
|
|
||||||
resolvedRoles
|
|
||||||
) serviceConfigs;
|
|
||||||
|
|
||||||
machinesRoles = builtins.zipAttrsWith (
|
|
||||||
_n: vs:
|
|
||||||
let
|
|
||||||
flat = builtins.foldl' (acc: s: acc ++ s.machines) [ ] vs;
|
|
||||||
in
|
|
||||||
lib.unique flat
|
|
||||||
) (builtins.attrValues config.resolvedRolesPerInstance);
|
|
||||||
|
|
||||||
assertions = lib.concatMapAttrs (
|
|
||||||
instanceName: resolvedRoles:
|
|
||||||
clanLib.modules.checkConstraints {
|
|
||||||
moduleName = serviceName;
|
|
||||||
allModules = inventory.modules;
|
|
||||||
inherit resolvedRoles instanceName;
|
|
||||||
}
|
|
||||||
) config.resolvedRolesPerInstance;
|
|
||||||
}
|
|
||||||
@@ -31,70 +31,13 @@ let
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
moduleConfig = lib.mkOption {
|
|
||||||
default = { };
|
|
||||||
# TODO: use types.deferredModule
|
|
||||||
# clan.borgbackup MUST be defined as submodule
|
|
||||||
type = types.attrsOf types.anything;
|
|
||||||
description = ''
|
|
||||||
Configuration of the specific clanModule.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Configuration is passed to the nixos configuration scoped to the module.
|
|
||||||
|
|
||||||
```nix
|
|
||||||
clan.<serviceName> = { ... # Config }
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
extraModulesOption = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
List of additionally imported `.nix` expressions.
|
|
||||||
|
|
||||||
Supported types:
|
|
||||||
|
|
||||||
- **Strings**: Interpreted relative to the 'directory' passed to `lib.clan`.
|
|
||||||
- **Paths**: should be relative to the current file.
|
|
||||||
- **Any**: Nix expression must be serializable to JSON.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
**The import only happens if the machine is part of the service or role.**
|
|
||||||
|
|
||||||
Other types are passed through to the nixos configuration.
|
|
||||||
|
|
||||||
???+ Example
|
|
||||||
To import the `special.nix` file
|
|
||||||
|
|
||||||
```
|
|
||||||
. Clan Directory
|
|
||||||
├── flake.nix
|
|
||||||
...
|
|
||||||
└── modules
|
|
||||||
├── special.nix
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
extraModules = [ "modules/special.nix" ];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
apply = value: if lib.isString value then value else builtins.seq (builtins.toJSON value) value;
|
|
||||||
default = [ ];
|
|
||||||
type = types.listOf (
|
|
||||||
types.oneOf [
|
|
||||||
types.str
|
|
||||||
types.anything
|
|
||||||
]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./assertions.nix
|
(lib.mkRemovedOptionModule [ "services" ] ''
|
||||||
|
The `inventory.services` option has been removed. Use `inventory.instances` instead.
|
||||||
|
See: https://docs.clan.lol/concepts/inventory/#services
|
||||||
|
'')
|
||||||
];
|
];
|
||||||
options = {
|
options = {
|
||||||
# Internal things
|
# Internal things
|
||||||
@@ -415,160 +358,5 @@ in
|
|||||||
);
|
);
|
||||||
default = { };
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
services = lib.mkOption {
|
|
||||||
# TODO: deprecate these options
|
|
||||||
# services are deprecated in favor of `instances`
|
|
||||||
# visible = false;
|
|
||||||
description = ''
|
|
||||||
Services of the inventory.
|
|
||||||
|
|
||||||
- The first `<name>` is the moduleName. It must be a valid clanModule name.
|
|
||||||
- The second `<name>` is an arbitrary instance name.
|
|
||||||
|
|
||||||
???+ Example
|
|
||||||
```nix
|
|
||||||
# ClanModule name. See the module documentation for the available modules.
|
|
||||||
# ↓ ↓ Instance name, can be anything, some services might use it as a unique identifier.
|
|
||||||
services.borgbackup."instance_1" = {
|
|
||||||
roles.client.machines = ["machineA"];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Services MUST be added to machines via `roles` exclusively.
|
|
||||||
See [`roles.<rolename>.machines`](#inventory.services.roles.machines) or [`roles.<rolename>.tags`](#inventory.services.roles.tags) for more information.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = types.attrsOf (
|
|
||||||
types.attrsOf (
|
|
||||||
types.submodule (
|
|
||||||
# instance name
|
|
||||||
{ name, ... }:
|
|
||||||
{
|
|
||||||
options.enabled = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
Enable or disable the complete service.
|
|
||||||
|
|
||||||
If the service is disabled, it will not be added to any machine.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
This flag is primarily used to temporarily disable a service.
|
|
||||||
I.e. A 'backup service' without any 'server' might be incomplete and would cause failure if enabled.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
options.meta = metaOptionsWith name;
|
|
||||||
options.extraModules = extraModulesOption;
|
|
||||||
options.config = moduleConfig // {
|
|
||||||
description = ''
|
|
||||||
Configuration of the specific clanModule.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Configuration is passed to the nixos configuration scoped to the module.
|
|
||||||
|
|
||||||
```nix
|
|
||||||
clan.<serviceName> = { ... # Config }
|
|
||||||
```
|
|
||||||
|
|
||||||
???+ Example
|
|
||||||
|
|
||||||
For `services.borgbackup` the config is the passed to the machine with the prefix of `clan.borgbackup`.
|
|
||||||
This means all config values are mapped to the `borgbackup` clanModule exclusively (`config.clan.borgbackup`).
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
services.borgbackup."instance_1".config = {
|
|
||||||
destinations = [ ... ];
|
|
||||||
# See the 'borgbackup' module docs for all options
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
The module author is responsible for supporting multiple instance configurations in different roles.
|
|
||||||
See each clanModule's documentation for more information.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
options.machines = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Attribute set of machines specific config for the service.
|
|
||||||
|
|
||||||
Will be merged with other service configs, such as the role config and the global config.
|
|
||||||
For machine specific overrides use `mkForce` or other higher priority methods.
|
|
||||||
|
|
||||||
???+ Example
|
|
||||||
|
|
||||||
```{.nix hl_lines="4-7"}
|
|
||||||
services.borgbackup."instance_1" = {
|
|
||||||
roles.client.machines = ["machineA"];
|
|
||||||
|
|
||||||
machines.machineA.config = {
|
|
||||||
# Additional specific config for the machine
|
|
||||||
# This is merged with all other config places
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = types.attrsOf (
|
|
||||||
types.submodule {
|
|
||||||
options.extraModules = extraModulesOption;
|
|
||||||
options.config = moduleConfig // {
|
|
||||||
description = ''
|
|
||||||
Additional configuration of the specific machine.
|
|
||||||
|
|
||||||
See how [`service.<name>.<name>.config`](#inventory.services.config) works in general for further information.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
options.roles = lib.mkOption {
|
|
||||||
default = { };
|
|
||||||
type = types.attrsOf (
|
|
||||||
types.submodule {
|
|
||||||
options.machines = lib.mkOption {
|
|
||||||
default = [ ];
|
|
||||||
type = types.listOf types.str;
|
|
||||||
example = [ "machineA" ];
|
|
||||||
description = ''
|
|
||||||
List of machines which are part of the role.
|
|
||||||
|
|
||||||
The machines are referenced by their `attributeName` in the `inventory.machines` attribute set.
|
|
||||||
|
|
||||||
Memberships are declared here to determine which machines are part of the service.
|
|
||||||
|
|
||||||
Alternatively, `tags` can be used to determine the membership, more dynamically.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
options.tags = lib.mkOption {
|
|
||||||
default = [ ];
|
|
||||||
apply = lib.unique;
|
|
||||||
type = types.listOf types.str;
|
|
||||||
description = ''
|
|
||||||
List of tags which are used to determine the membership of the role.
|
|
||||||
|
|
||||||
The tags are matched against the `inventory.machines.<machineName>.tags` attribute set.
|
|
||||||
If a machine has at least one tag of the role, it is part of the role.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
options.config = moduleConfig // {
|
|
||||||
description = ''
|
|
||||||
Additional configuration of the specific role.
|
|
||||||
|
|
||||||
See how [`service.<name>.<name>.config`](#inventory.services.config) works in general for further information.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
options.extraModules = extraModulesOption;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
default =
|
default =
|
||||||
builtins.removeAttrs (clanLib.introspection.getPrios { options = config.inventory.options; })
|
builtins.removeAttrs (clanLib.introspection.getPrios { options = config.inventory.options; })
|
||||||
# tags are freeformType which is not supported yet.
|
# tags are freeformType which is not supported yet.
|
||||||
[ "tags" ];
|
# services is removed and throws an error if accessed.
|
||||||
|
[
|
||||||
|
"tags"
|
||||||
|
"services"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { API } from "@/api/API";
|
import { API } from "@/api/API";
|
||||||
import { Schema as Inventory } from "@/api/Inventory";
|
|
||||||
|
|
||||||
export type OperationNames = keyof API;
|
export type OperationNames = keyof API;
|
||||||
type Services = NonNullable<Inventory["services"]>;
|
|
||||||
type ServiceNames = keyof Services;
|
|
||||||
|
|
||||||
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
|
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
|
||||||
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
||||||
|
|
||||||
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
|
|
||||||
Services[T]
|
|
||||||
>[string];
|
|
||||||
|
|
||||||
export type SuccessQuery<T extends OperationNames> = Extract<
|
export type SuccessQuery<T extends OperationNames> = Extract<
|
||||||
OperationResponse<T>,
|
OperationResponse<T>,
|
||||||
{ status: "success" }
|
{ status: "success" }
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
from clan_lib.nix_models.clan import Inventory
|
|
||||||
from clan_lib.nix_models.clan import InventoryMachine as Machine
|
|
||||||
from clan_lib.nix_models.clan import InventoryMeta as Meta
|
|
||||||
from clan_lib.nix_models.clan import InventoryService as Service
|
|
||||||
|
|
||||||
|
|
||||||
def test_make_meta_minimal() -> None:
|
|
||||||
# Name is required
|
|
||||||
res = Meta(
|
|
||||||
{
|
|
||||||
"name": "foo",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert res == {"name": "foo"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_make_inventory_minimal() -> None:
|
|
||||||
# Meta is required
|
|
||||||
res = Inventory(
|
|
||||||
{
|
|
||||||
"meta": Meta(
|
|
||||||
{
|
|
||||||
"name": "foo",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert res == {"meta": {"name": "foo"}}
|
|
||||||
|
|
||||||
|
|
||||||
def test_make_machine_minimal() -> None:
|
|
||||||
# Empty is valid
|
|
||||||
res = Machine({})
|
|
||||||
|
|
||||||
assert res == {}
|
|
||||||
|
|
||||||
|
|
||||||
def test_make_service_minimal() -> None:
|
|
||||||
# Empty is valid
|
|
||||||
res = Service({})
|
|
||||||
|
|
||||||
assert res == {}
|
|
||||||
@@ -53,7 +53,6 @@ def test_inventory_deserialize_variants(
|
|||||||
# Check that all keys are present
|
# Check that all keys are present
|
||||||
assert "meta" in inventory
|
assert "meta" in inventory
|
||||||
assert "machines" in inventory
|
assert "machines" in inventory
|
||||||
assert "services" in inventory
|
|
||||||
# assert "tags" in inventory
|
# assert "tags" in inventory
|
||||||
# assert "modules" in inventory
|
# assert "modules" in inventory
|
||||||
assert "instances" in inventory
|
assert "instances" in inventory
|
||||||
|
|||||||
@@ -97,18 +97,10 @@ class InventoryMeta(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryService(TypedDict):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
InventoryInstancesType = dict[str, InventoryInstance]
|
InventoryInstancesType = dict[str, InventoryInstance]
|
||||||
InventoryMachinesType = dict[str, InventoryMachine]
|
InventoryMachinesType = dict[str, InventoryMachine]
|
||||||
InventoryMetaType = InventoryMeta
|
InventoryMetaType = InventoryMeta
|
||||||
InventoryModulesType = dict[str, dict[str, Any] | list[Any] | bool | float | int | str | None]
|
InventoryModulesType = dict[str, dict[str, Any] | list[Any] | bool | float | int | str | None]
|
||||||
InventoryServicesType = dict[str, InventoryService]
|
|
||||||
InventoryTagsType = dict[str, list[str]]
|
InventoryTagsType = dict[str, list[str]]
|
||||||
|
|
||||||
class Inventory(TypedDict):
|
class Inventory(TypedDict):
|
||||||
@@ -116,7 +108,6 @@ class Inventory(TypedDict):
|
|||||||
machines: NotRequired[InventoryMachinesType]
|
machines: NotRequired[InventoryMachinesType]
|
||||||
meta: NotRequired[InventoryMetaType]
|
meta: NotRequired[InventoryMetaType]
|
||||||
modules: NotRequired[InventoryModulesType]
|
modules: NotRequired[InventoryModulesType]
|
||||||
services: NotRequired[InventoryServicesType]
|
|
||||||
tags: NotRequired[InventoryTagsType]
|
tags: NotRequired[InventoryTagsType]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from clan_lib.nix_models.clan import (
|
|||||||
InventoryInstancesType,
|
InventoryInstancesType,
|
||||||
InventoryMachinesType,
|
InventoryMachinesType,
|
||||||
InventoryMetaType,
|
InventoryMetaType,
|
||||||
InventoryServicesType,
|
|
||||||
InventoryTagsType,
|
InventoryTagsType,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -106,7 +105,6 @@ class InventorySnapshot(TypedDict):
|
|||||||
machines: NotRequired[InventoryMachinesType]
|
machines: NotRequired[InventoryMachinesType]
|
||||||
instances: NotRequired[InventoryInstancesType]
|
instances: NotRequired[InventoryInstancesType]
|
||||||
meta: NotRequired[InventoryMetaType]
|
meta: NotRequired[InventoryMetaType]
|
||||||
services: NotRequired[InventoryServicesType]
|
|
||||||
tags: NotRequired[InventoryTagsType]
|
tags: NotRequired[InventoryTagsType]
|
||||||
|
|
||||||
|
|
||||||
@@ -163,7 +161,8 @@ class InventoryStore:
|
|||||||
return sanitized
|
return sanitized
|
||||||
|
|
||||||
def get_readonly_raw(self) -> Inventory:
|
def get_readonly_raw(self) -> Inventory:
|
||||||
return self._flake.select("clanInternals.inventoryClass.inventory")
|
attrs = "{" + ",".join(self._keys) + "}"
|
||||||
|
return self._flake.select(f"clanInternals.inventoryClass.inventory.{attrs}")
|
||||||
|
|
||||||
def _get_persisted(self) -> InventorySnapshot:
|
def _get_persisted(self) -> InventorySnapshot:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ def test_simple_read_write(setup_test_files: Path) -> None:
|
|||||||
store = InventoryStore(
|
store = InventoryStore(
|
||||||
flake=MockFlake(nix_file),
|
flake=MockFlake(nix_file),
|
||||||
inventory_file_name=json_file.name,
|
inventory_file_name=json_file.name,
|
||||||
_keys=[], # disable toplevel filtering
|
_keys=["foo", "protected"],
|
||||||
)
|
)
|
||||||
store._flake.invalidate_cache()
|
store._flake.invalidate_cache()
|
||||||
data: dict = store.read() # type: ignore
|
data: dict = store.read() # type: ignore
|
||||||
@@ -149,7 +149,7 @@ def test_simple_deferred(setup_test_files: Path) -> None:
|
|||||||
inventory_file_name=json_file.name,
|
inventory_file_name=json_file.name,
|
||||||
# Needed to allow auto-transforming deferred modules
|
# Needed to allow auto-transforming deferred modules
|
||||||
_allowed_path_transforms=["foo.*"],
|
_allowed_path_transforms=["foo.*"],
|
||||||
_keys=[], # disable toplevel filtering
|
_keys=["foo"], # disable toplevel filtering
|
||||||
)
|
)
|
||||||
|
|
||||||
data = store.read()
|
data = store.read()
|
||||||
@@ -230,7 +230,7 @@ def test_manipulate_list(setup_test_files: Path) -> None:
|
|||||||
store = InventoryStore(
|
store = InventoryStore(
|
||||||
flake=MockFlake(nix_file),
|
flake=MockFlake(nix_file),
|
||||||
inventory_file_name=json_file.name,
|
inventory_file_name=json_file.name,
|
||||||
_keys=[], # disable toplevel filtering
|
_keys=["empty", "predefined"],
|
||||||
)
|
)
|
||||||
|
|
||||||
data = store.read()
|
data = store.read()
|
||||||
@@ -275,7 +275,7 @@ def test_static_list_items(setup_test_files: Path) -> None:
|
|||||||
store = InventoryStore(
|
store = InventoryStore(
|
||||||
flake=MockFlake(nix_file),
|
flake=MockFlake(nix_file),
|
||||||
inventory_file_name=json_file.name,
|
inventory_file_name=json_file.name,
|
||||||
_keys=[], # disable toplevel filtering
|
_keys=["empty", "predefined"],
|
||||||
)
|
)
|
||||||
|
|
||||||
data = store.read()
|
data = store.read()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, cast
|
from typing import cast
|
||||||
|
|
||||||
import clan_cli.clan.create
|
import clan_cli.clan.create
|
||||||
import pytest
|
import pytest
|
||||||
@@ -26,7 +26,6 @@ from clan_lib.nix import nix_command
|
|||||||
from clan_lib.nix_models.clan import (
|
from clan_lib.nix_models.clan import (
|
||||||
InventoryInstancesType,
|
InventoryInstancesType,
|
||||||
InventoryMachine,
|
InventoryMachine,
|
||||||
InventoryServicesType,
|
|
||||||
Unknown,
|
Unknown,
|
||||||
)
|
)
|
||||||
from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy
|
from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy
|
||||||
@@ -41,7 +40,6 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class InventoryWrapper:
|
class InventoryWrapper:
|
||||||
services: InventoryServicesType
|
|
||||||
instances: InventoryInstancesType
|
instances: InventoryInstancesType
|
||||||
|
|
||||||
|
|
||||||
@@ -65,8 +63,6 @@ def create_base_inventory(ssh_keys_pairs: list[SSHKeyPair]) -> InventoryWrapper:
|
|||||||
ssh_keys.append(InvSSHKeyEntry(f"user_{num}", ssh_key.public.read_text()))
|
ssh_keys.append(InvSSHKeyEntry(f"user_{num}", ssh_key.public.read_text()))
|
||||||
|
|
||||||
"""Create the base inventory structure."""
|
"""Create the base inventory structure."""
|
||||||
legacy_services: dict[str, Any] = {}
|
|
||||||
|
|
||||||
instances = InventoryInstancesType(
|
instances = InventoryInstancesType(
|
||||||
{
|
{
|
||||||
"admin-inst": {
|
"admin-inst": {
|
||||||
@@ -88,7 +84,7 @@ def create_base_inventory(ssh_keys_pairs: list[SSHKeyPair]) -> InventoryWrapper:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return InventoryWrapper(services=legacy_services, instances=instances)
|
return InventoryWrapper(instances=instances)
|
||||||
|
|
||||||
|
|
||||||
# TODO: We need a way to calculate the narHash of the current clan-core
|
# TODO: We need a way to calculate the narHash of the current clan-core
|
||||||
@@ -212,7 +208,6 @@ def test_clan_create_api(
|
|||||||
== "clan-core/admin"
|
== "clan-core/admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
set_value_by_path(inventory, "services", inventory_conf.services)
|
|
||||||
set_value_by_path(inventory, "instances", inventory_conf.instances)
|
set_value_by_path(inventory, "instances", inventory_conf.instances)
|
||||||
store.write(
|
store.write(
|
||||||
inventory,
|
inventory,
|
||||||
|
|||||||
@@ -100,12 +100,6 @@
|
|||||||
# It treats it not as the type of an empty object, but as non-nullish.
|
# It treats it not as the type of an empty object, but as non-nullish.
|
||||||
# Should be fixed in json2ts: https://github.com/bcherny/json-schema-to-typescript/issues/557
|
# Should be fixed in json2ts: https://github.com/bcherny/json-schema-to-typescript/issues/557
|
||||||
sed -i -e 's/{}/Record<string, never>/g' $out/API.ts
|
sed -i -e 's/{}/Record<string, never>/g' $out/API.ts
|
||||||
|
|
||||||
# Retrieve python API Typescript types
|
|
||||||
# delete the reserved tags from typechecking because the conversion library doesn't support them
|
|
||||||
jq 'del(.properties.tags.properties)' ${self'.legacyPackages.schemas.inventory}/schema.json > schema.json
|
|
||||||
json2ts --input schema.json > $out/Inventory.ts
|
|
||||||
cp ${self'.legacyPackages.schemas.inventory}/* $out
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
clan-lib-openapi = pkgs.stdenv.mkDerivation {
|
clan-lib-openapi = pkgs.stdenv.mkDerivation {
|
||||||
|
|||||||
Reference in New Issue
Block a user