Compare commits
30 Commits
hgl-site
...
write-acce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f13ce2732 | ||
|
|
1465b18820 | ||
|
|
6fa0062573 | ||
|
|
6cd68c23f5 | ||
|
|
fdddc60676 | ||
|
|
684aa27068 | ||
|
|
35d8deb393 | ||
|
|
e2f20b5ffc | ||
|
|
fd5d7934a0 | ||
|
|
f194c31e0e | ||
|
|
061b598adf | ||
|
|
744f35e0cc | ||
|
|
4a6d46198c | ||
|
|
82d5ca9a0b | ||
|
|
28d8a91a30 | ||
|
|
18f8d69728 | ||
|
|
1feead4ce4 | ||
|
|
7f28110558 | ||
|
|
38787da891 | ||
|
|
2b587da9fe | ||
|
|
acd2c1654b | ||
|
|
2ecb1399c3 | ||
|
|
46ae6b49c1 | ||
|
|
50a8a69719 | ||
|
|
203761a99c | ||
|
|
990b4e0223 | ||
|
|
032f54cbfb | ||
|
|
47146efa0f | ||
|
|
c031abcd9e | ||
|
|
0390d5999d |
12
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
12
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## Description of the change
|
||||
|
||||
<!-- Brief summary of the change if not already clear from the title -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Updated Documentation
|
||||
- [ ] Added tests
|
||||
- [ ] Doesn't affect backwards compatibility - or check the next points
|
||||
- [ ] Add the breaking change and migration details to docs/release-notes.md
|
||||
- !!! Review from another person is required *BEFORE* merge !!!
|
||||
- [ ] Add introduction of major feature to docs/release-notes.md
|
||||
@@ -120,7 +120,7 @@ in
|
||||
) (self.darwinConfigurations or { })
|
||||
// lib.mapAttrs' (n: lib.nameValuePair "package-${n}") (
|
||||
if system == "aarch64-darwin" then
|
||||
lib.filterAttrs (n: _: n != "docs" && n != "deploy-docs" && n != "docs-options") packagesToBuild
|
||||
lib.filterAttrs (n: _: n != "docs" && n != "deploy-docs" && n != "option-search") packagesToBuild
|
||||
else
|
||||
packagesToBuild
|
||||
)
|
||||
|
||||
12
devFlake/flake.lock
generated
12
devFlake/flake.lock
generated
@@ -3,10 +3,10 @@
|
||||
"clan-core-for-checks": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1759915474,
|
||||
"narHash": "sha256-ef7awwmx2onWuA83FNE29B3tTZ+tQxEWLD926ckMiF8=",
|
||||
"lastModified": 1760000589,
|
||||
"narHash": "sha256-9xBwxeb8x5XOo3alaJvv2ZwL7UhW3/oYUUBK+odWGrk=",
|
||||
"ref": "main",
|
||||
"rev": "81e15cab34f9ae00b6f2df5f2e53ee07cd3a0af3",
|
||||
"rev": "e2f20b5ffcd4ff59e2528d29649056e3eb8d22bb",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "https://git.clan.lol/clan/clan-core"
|
||||
@@ -105,11 +105,11 @@
|
||||
},
|
||||
"nixpkgs-dev": {
|
||||
"locked": {
|
||||
"lastModified": 1759860509,
|
||||
"narHash": "sha256-c7eJvqAlWLhwNc9raHkQ7mvoFbHLUO/cLMrww1ds4Zg=",
|
||||
"lastModified": 1759989671,
|
||||
"narHash": "sha256-3Wk0I5TYsd7cyIO8vYGxjOuQ8zraZEUFZqEhSSIhQLs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b574dcadf3fb578dee8d104b565bd745a5a9edc0",
|
||||
"rev": "837076de579c67aa0c2ce2ab49948b24d907d449",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
2
docs/.gitignore
vendored
2
docs/.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
/site/reference
|
||||
/site/services/official
|
||||
/site/static
|
||||
/site/options
|
||||
/site/option-search
|
||||
/site/openapi.json
|
||||
!/site/static/extra.css
|
||||
|
||||
@@ -180,7 +180,7 @@ nav:
|
||||
- services/official/zerotier.md
|
||||
- services/community.md
|
||||
|
||||
- Search Clan Options: "/options"
|
||||
- Search Clan Options: "/option-search"
|
||||
|
||||
docs_dir: site
|
||||
site_dir: out
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
clan-lib-openapi,
|
||||
roboto,
|
||||
fira-code,
|
||||
docs-options,
|
||||
option-search,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -51,9 +51,9 @@ pkgs.stdenv.mkDerivation {
|
||||
chmod -R +w ./site
|
||||
echo "Generated API documentation in './site/reference/' "
|
||||
|
||||
rm -rf ./site/options
|
||||
cp -r ${docs-options} ./site/options
|
||||
chmod -R +w ./site/options
|
||||
rm -rf ./site/option-search
|
||||
cp -r ${option-search} ./site/option-search
|
||||
chmod -R +w ./site/option-search
|
||||
|
||||
# Link to fonts
|
||||
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{ inputs, self, ... }:
|
||||
{ inputs, ... }:
|
||||
{
|
||||
imports = [
|
||||
./options/flake-module.nix
|
||||
];
|
||||
perSystem =
|
||||
{
|
||||
config,
|
||||
@@ -10,74 +7,7 @@
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# Simply evaluated options (JSON)
|
||||
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
|
||||
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
|
||||
inherit (self) clanModules;
|
||||
clan-core = self;
|
||||
inherit pkgs;
|
||||
};
|
||||
|
||||
# clan service options
|
||||
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
|
||||
|
||||
# Simply evaluated options (JSON)
|
||||
renderOptions =
|
||||
pkgs.runCommand "render-options"
|
||||
{
|
||||
# TODO: ruff does not splice properly in nativeBuildInputs
|
||||
depsBuildBuild = [ pkgs.ruff ];
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.mypy
|
||||
self'.packages.clan-cli
|
||||
];
|
||||
}
|
||||
''
|
||||
install -D -m755 ${./render_options}/__init__.py $out/bin/render-options
|
||||
patchShebangs --build $out/bin/render-options
|
||||
|
||||
ruff format --check --diff $out/bin/render-options
|
||||
ruff check --line-length 88 $out/bin/render-options
|
||||
mypy --strict $out/bin/render-options
|
||||
'';
|
||||
|
||||
module-docs =
|
||||
pkgs.runCommand "rendered"
|
||||
{
|
||||
buildInputs = [
|
||||
pkgs.python3
|
||||
self'.packages.clan-cli
|
||||
];
|
||||
}
|
||||
''
|
||||
export CLAN_CORE_PATH=${
|
||||
inputs.nixpkgs.lib.fileset.toSource {
|
||||
root = ../..;
|
||||
fileset = ../../clanModules;
|
||||
}
|
||||
}
|
||||
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
||||
|
||||
# A file that contains the links to all clanModule docs
|
||||
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
|
||||
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
|
||||
export CLAN_OPTIONS_PATH=${self'.legacyPackages.clan-options}/share/doc/nixos/options.json
|
||||
|
||||
mkdir $out
|
||||
|
||||
# The python script will place mkDocs files in the output directory
|
||||
exec python3 ${renderOptions}/bin/render-options
|
||||
'';
|
||||
in
|
||||
{
|
||||
legacyPackages = {
|
||||
inherit
|
||||
jsonDocs
|
||||
clanModulesViaService
|
||||
;
|
||||
};
|
||||
devShells.docs = self'.packages.docs.overrideAttrs (_old: {
|
||||
nativeBuildInputs = [
|
||||
# Run: htmlproofer --disable-external
|
||||
@@ -96,15 +26,14 @@
|
||||
docs = pkgs.python3.pkgs.callPackage ./default.nix {
|
||||
inherit (self'.packages)
|
||||
clan-cli-docs
|
||||
docs-options
|
||||
option-search
|
||||
inventory-api-docs
|
||||
clan-lib-openapi
|
||||
module-docs
|
||||
;
|
||||
inherit (inputs) nixpkgs;
|
||||
inherit module-docs;
|
||||
};
|
||||
deploy-docs = pkgs.callPackage ./deploy-docs.nix { inherit (config.packages) docs; };
|
||||
inherit module-docs;
|
||||
};
|
||||
checks.docs-integrity =
|
||||
pkgs.runCommand "docs-integrity"
|
||||
|
||||
9
docs/release-notes.md
Normal file
9
docs/release-notes.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# clan-core release notes 25.11
|
||||
|
||||
<!-- This is not rendered yet -->
|
||||
|
||||
## New features
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
## Misc
|
||||
@@ -288,7 +288,7 @@ of their type.
|
||||
In the inventory we the assign machines to a type, e.g. by using tags
|
||||
|
||||
```nix title="flake.nix"
|
||||
instnaces.machine-type = {
|
||||
instances.machine-type = {
|
||||
module.input = "self";
|
||||
module.name = "@pinpox/machine-type";
|
||||
roles.desktop.tags.desktop = { };
|
||||
|
||||
@@ -70,6 +70,8 @@ hide:
|
||||
.clamp-toggle:checked ~ .clamp-more::after { content: "Read less"; }
|
||||
</style>
|
||||
|
||||
trivial change
|
||||
|
||||
<div class="clamp-wrap" style="--lines: 3;">
|
||||
<input type="checkbox" id="clan-readmore" class="clamp-toggle" />
|
||||
<div class="clamp-content">
|
||||
@@ -122,7 +124,7 @@ hide:
|
||||
|
||||
command line interface
|
||||
|
||||
- [Clan Options](/options)
|
||||
- [Clan Options](./reference/options/clan.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ This section of the site provides an overview of available options and commands
|
||||
|
||||
---
|
||||
|
||||
- [Clan Configuration Option](/options) - for defining a Clan
|
||||
- Learn how to use the [Clan CLI](../reference/cli/index.md)
|
||||
- Explore available [services](../services/definition.md)
|
||||
- [NixOS Configuration Options](../reference/clan.core/index.md) - Additional options avilable on a NixOS machine.
|
||||
- [Search Clan Option](/option-search) - for defining a Clan
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# tests for the nixos options to jsonschema converter
|
||||
# run these tests via `nix-unit ./test.nix`
|
||||
{
|
||||
lib ? (import <nixpkgs> { }).lib,
|
||||
lib ? import /home/johannes/git/nixpkgs/lib,
|
||||
# lib ? (import <nixpkgs> { }).lib,
|
||||
slib ? (import ./. { inherit lib; }),
|
||||
}:
|
||||
let
|
||||
@@ -67,31 +68,38 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
test_no_default = {
|
||||
expr = stableView (
|
||||
slib.getPrios {
|
||||
options =
|
||||
(eval [
|
||||
{
|
||||
options.foo.bar = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
};
|
||||
}
|
||||
]).options;
|
||||
}
|
||||
);
|
||||
expected = {
|
||||
foo = {
|
||||
bar = {
|
||||
__this = {
|
||||
files = [ ];
|
||||
prio = 9999;
|
||||
total = false;
|
||||
test_no_default =
|
||||
let
|
||||
|
||||
configuration = (
|
||||
eval [
|
||||
{
|
||||
options.foo.bar = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
};
|
||||
}
|
||||
]
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit configuration;
|
||||
expr = stableView (
|
||||
slib.getPrios {
|
||||
options = configuration.options;
|
||||
}
|
||||
);
|
||||
expected = {
|
||||
foo = {
|
||||
bar = {
|
||||
__this = {
|
||||
files = [ ];
|
||||
prio = 9999;
|
||||
total = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
test_submodule = {
|
||||
expr = stableView (
|
||||
|
||||
@@ -11,28 +11,35 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<ClanSettingsModalProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
onClose: fn(),
|
||||
model: {
|
||||
uri: "/home/foo/my-clan",
|
||||
const props: ClanSettingsModalProps = {
|
||||
onClose: fn(),
|
||||
model: {
|
||||
uri: "/home/foo/my-clan",
|
||||
details: {
|
||||
name: "Sol",
|
||||
description: null,
|
||||
icon: null,
|
||||
fieldsSchema: {
|
||||
name: {
|
||||
readonly: true,
|
||||
reason: null,
|
||||
},
|
||||
description: {
|
||||
readonly: false,
|
||||
reason: null,
|
||||
},
|
||||
icon: {
|
||||
readonly: false,
|
||||
reason: null,
|
||||
},
|
||||
},
|
||||
fieldsSchema: {
|
||||
name: {
|
||||
readonly: true,
|
||||
reason: null,
|
||||
readonly_members: [],
|
||||
},
|
||||
description: {
|
||||
readonly: false,
|
||||
reason: null,
|
||||
readonly_members: [],
|
||||
},
|
||||
icon: {
|
||||
readonly: false,
|
||||
reason: null,
|
||||
readonly_members: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: props,
|
||||
};
|
||||
|
||||
@@ -166,16 +166,16 @@ def test_generate_public_and_secret_vars(
|
||||
assert shared_value.startswith("shared")
|
||||
vars_text = stringify_all_vars(machine)
|
||||
flake_obj = Flake(str(flake.path))
|
||||
my_generator = Generator("my_generator", machine="my_machine", _flake=flake_obj)
|
||||
my_generator = Generator("my_generator", machines=["my_machine"], _flake=flake_obj)
|
||||
shared_generator = Generator(
|
||||
"my_shared_generator",
|
||||
share=True,
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
dependent_generator = Generator(
|
||||
"dependent_generator",
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
in_repo_store = in_repo.FactStore(flake=flake_obj)
|
||||
@@ -340,12 +340,12 @@ def test_generate_secret_var_sops_with_default_group(
|
||||
flake_obj = Flake(str(flake.path))
|
||||
first_generator = Generator(
|
||||
"first_generator",
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
second_generator = Generator(
|
||||
"second_generator",
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
in_repo_store = in_repo.FactStore(flake=flake_obj)
|
||||
@@ -375,13 +375,13 @@ def test_generate_secret_var_sops_with_default_group(
|
||||
first_generator_with_share = Generator(
|
||||
"first_generator",
|
||||
share=False,
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
second_generator_with_share = Generator(
|
||||
"second_generator",
|
||||
share=False,
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
assert sops_store.user_has_access("user2", first_generator_with_share, "my_secret")
|
||||
@@ -432,7 +432,6 @@ def test_generated_shared_secret_sops(
|
||||
assert check_vars(machine1.name, machine1.flake)
|
||||
cli.run(["vars", "generate", "--flake", str(flake.path), "machine2"])
|
||||
assert check_vars(machine2.name, machine2.flake)
|
||||
assert check_vars(machine2.name, machine2.flake)
|
||||
m1_sops_store = sops.SecretStore(machine1.flake)
|
||||
m2_sops_store = sops.SecretStore(machine2.flake)
|
||||
# Create generators with machine context for testing
|
||||
@@ -513,28 +512,28 @@ def test_generate_secret_var_password_store(
|
||||
"my_generator",
|
||||
share=False,
|
||||
files=[],
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
my_generator_shared = Generator(
|
||||
"my_generator",
|
||||
share=True,
|
||||
files=[],
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
my_shared_generator = Generator(
|
||||
"my_shared_generator",
|
||||
share=True,
|
||||
files=[],
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
my_shared_generator_not_shared = Generator(
|
||||
"my_shared_generator",
|
||||
share=False,
|
||||
files=[],
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
assert store.exists(my_generator, "my_secret")
|
||||
@@ -546,7 +545,7 @@ def test_generate_secret_var_password_store(
|
||||
name="my_generator",
|
||||
share=False,
|
||||
files=[],
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
assert store.get(generator, "my_secret").decode() == "hello\n"
|
||||
@@ -557,7 +556,7 @@ def test_generate_secret_var_password_store(
|
||||
"my_generator",
|
||||
share=False,
|
||||
files=[],
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
var_name = "my_secret"
|
||||
@@ -570,7 +569,7 @@ def test_generate_secret_var_password_store(
|
||||
"my_generator2",
|
||||
share=False,
|
||||
files=[],
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
var_name = "my_secret2"
|
||||
@@ -582,7 +581,7 @@ def test_generate_secret_var_password_store(
|
||||
"my_shared_generator",
|
||||
share=True,
|
||||
files=[],
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
var_name = "my_shared_secret"
|
||||
@@ -629,8 +628,8 @@ def test_generate_secret_for_multiple_machines(
|
||||
in_repo_store2 = in_repo.FactStore(flake=flake_obj)
|
||||
|
||||
# Create generators for each machine
|
||||
gen1 = Generator("my_generator", machine="machine1", _flake=flake_obj)
|
||||
gen2 = Generator("my_generator", machine="machine2", _flake=flake_obj)
|
||||
gen1 = Generator("my_generator", machines=["machine1"], _flake=flake_obj)
|
||||
gen2 = Generator("my_generator", machines=["machine2"], _flake=flake_obj)
|
||||
|
||||
assert in_repo_store1.exists(gen1, "my_value")
|
||||
assert in_repo_store2.exists(gen2, "my_value")
|
||||
@@ -694,12 +693,12 @@ def test_prompt(
|
||||
|
||||
# Set up objects for testing the results
|
||||
flake_obj = Flake(str(flake.path))
|
||||
my_generator = Generator("my_generator", machine="my_machine", _flake=flake_obj)
|
||||
my_generator = Generator("my_generator", machines=["my_machine"], _flake=flake_obj)
|
||||
my_generator_with_details = Generator(
|
||||
name="my_generator",
|
||||
share=False,
|
||||
files=[],
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
|
||||
@@ -784,10 +783,10 @@ def test_shared_vars_regeneration(
|
||||
in_repo_store_2 = in_repo.FactStore(machine2.flake)
|
||||
# Create generators with machine context for testing
|
||||
child_gen_m1 = Generator(
|
||||
"child_generator", share=False, machine="machine1", _flake=machine1.flake
|
||||
"child_generator", share=False, machines=["machine1"], _flake=machine1.flake
|
||||
)
|
||||
child_gen_m2 = Generator(
|
||||
"child_generator", share=False, machine="machine2", _flake=machine2.flake
|
||||
"child_generator", share=False, machines=["machine2"], _flake=machine2.flake
|
||||
)
|
||||
# generate for machine 1
|
||||
cli.run(["vars", "generate", "--flake", str(flake.path), "machine1"])
|
||||
@@ -855,13 +854,13 @@ def test_multi_machine_shared_vars(
|
||||
generator_m1 = Generator(
|
||||
"shared_generator",
|
||||
share=True,
|
||||
machine="machine1",
|
||||
machines=["machine1"],
|
||||
_flake=machine1.flake,
|
||||
)
|
||||
generator_m2 = Generator(
|
||||
"shared_generator",
|
||||
share=True,
|
||||
machine="machine2",
|
||||
machines=["machine2"],
|
||||
_flake=machine2.flake,
|
||||
)
|
||||
# generate for machine 1
|
||||
@@ -917,7 +916,9 @@ def test_api_set_prompts(
|
||||
)
|
||||
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
|
||||
store = in_repo.FactStore(machine.flake)
|
||||
my_generator = Generator("my_generator", machine="my_machine", _flake=machine.flake)
|
||||
my_generator = Generator(
|
||||
"my_generator", machines=["my_machine"], _flake=machine.flake
|
||||
)
|
||||
assert store.exists(my_generator, "prompt1")
|
||||
assert store.get(my_generator, "prompt1").decode() == "input1"
|
||||
run_generators(
|
||||
@@ -1061,10 +1062,10 @@ def test_migration(
|
||||
assert "Migrated var my_generator/my_value" in caplog.text
|
||||
assert "Migrated secret var my_generator/my_secret" in caplog.text
|
||||
flake_obj = Flake(str(flake.path))
|
||||
my_generator = Generator("my_generator", machine="my_machine", _flake=flake_obj)
|
||||
my_generator = Generator("my_generator", machines=["my_machine"], _flake=flake_obj)
|
||||
other_generator = Generator(
|
||||
"other_generator",
|
||||
machine="my_machine",
|
||||
machines=["my_machine"],
|
||||
_flake=flake_obj,
|
||||
)
|
||||
in_repo_store = in_repo.FactStore(flake=flake_obj)
|
||||
@@ -1210,7 +1211,7 @@ def test_share_mode_switch_regenerates_secret(
|
||||
sops_store = sops.SecretStore(flake=flake_obj)
|
||||
|
||||
generator_not_shared = Generator(
|
||||
"my_generator", share=False, machine="my_machine", _flake=flake_obj
|
||||
"my_generator", share=False, machines=["my_machine"], _flake=flake_obj
|
||||
)
|
||||
|
||||
initial_public = in_repo_store.get(generator_not_shared, "my_value").decode()
|
||||
@@ -1229,7 +1230,7 @@ def test_share_mode_switch_regenerates_secret(
|
||||
|
||||
# Read the new values with shared generator
|
||||
generator_shared = Generator(
|
||||
"my_generator", share=True, machine="my_machine", _flake=flake_obj
|
||||
"my_generator", share=True, machines=["my_machine"], _flake=flake_obj
|
||||
)
|
||||
|
||||
new_public = in_repo_store.get(generator_shared, "my_value").decode()
|
||||
|
||||
@@ -40,12 +40,15 @@ class StoreBase(ABC):
|
||||
|
||||
def get_machine(self, generator: "Generator") -> str:
|
||||
"""Get machine name from generator, asserting it's not None for now."""
|
||||
if generator.machine is None:
|
||||
if generator.share:
|
||||
return "__shared"
|
||||
if generator.share:
|
||||
return "__shared"
|
||||
if not generator.machines:
|
||||
msg = f"Generator '{generator.name}' has no machine associated"
|
||||
raise ClanError(msg)
|
||||
return generator.machine
|
||||
if len(generator.machines) != 1:
|
||||
msg = f"Generator '{generator.name}' has {len(generator.machines)} machines, expected exactly 1"
|
||||
raise ClanError(msg)
|
||||
return generator.machines[0]
|
||||
|
||||
# get a single fact
|
||||
@abstractmethod
|
||||
@@ -147,7 +150,7 @@ class StoreBase(ABC):
|
||||
prev_generator = dataclasses.replace(
|
||||
generator,
|
||||
share=not generator.share,
|
||||
machine=machine if generator.share else None,
|
||||
machines=[] if not generator.share else [machine],
|
||||
)
|
||||
if self.exists(prev_generator, var.name):
|
||||
changed_files += self.delete(prev_generator, var.name)
|
||||
@@ -165,12 +168,12 @@ class StoreBase(ABC):
|
||||
new_file = self._set(generator, var, value, machine)
|
||||
action_str = "Migrated" if is_migration else "Updated"
|
||||
log_info: Callable
|
||||
if generator.machine is None:
|
||||
if generator.share:
|
||||
log_info = log.info
|
||||
else:
|
||||
from clan_lib.machines.machines import Machine # noqa: PLC0415
|
||||
|
||||
machine_obj = Machine(name=generator.machine, flake=self.flake)
|
||||
machine_obj = Machine(name=generator.machines[0], flake=self.flake)
|
||||
log_info = machine_obj.info
|
||||
if self.is_secret_store:
|
||||
log.info(f"{action_str} secret var {generator.name}/{var.name}\n")
|
||||
|
||||
@@ -61,14 +61,22 @@ class Generator:
|
||||
migrate_fact: str | None = None
|
||||
validation_hash: str | None = None
|
||||
|
||||
machine: str | None = None
|
||||
machines: list[str] = field(default_factory=list)
|
||||
_flake: "Flake | None" = None
|
||||
_public_store: "StoreBase | None" = None
|
||||
_secret_store: "StoreBase | None" = None
|
||||
|
||||
@property
|
||||
def key(self) -> GeneratorKey:
|
||||
return GeneratorKey(machine=self.machine, name=self.name)
|
||||
if self.share:
|
||||
# must be a shared generator
|
||||
machine = None
|
||||
elif len(self.machines) != 1:
|
||||
msg = f"Shared generator {self.name} must have exactly one machine, but has {len(self.machines)}: {', '.join(self.machines)}"
|
||||
raise ClanError(msg)
|
||||
else:
|
||||
machine = self.machines[0]
|
||||
return GeneratorKey(machine=machine, name=self.name)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.key)
|
||||
@@ -143,7 +151,7 @@ class Generator:
|
||||
files_selector = "config.clan.core.vars.generators.*.files.*.{secret,deploy,owner,group,mode,neededFor}"
|
||||
flake.precache(cls.get_machine_selectors(machine_names))
|
||||
|
||||
generators = []
|
||||
generators: list[Generator] = []
|
||||
shared_generators_raw: dict[
|
||||
str, tuple[str, dict, dict]
|
||||
] = {} # name -> (machine_name, gen_data, files_data)
|
||||
@@ -244,15 +252,27 @@ class Generator:
|
||||
migrate_fact=gen_data.get("migrateFact"),
|
||||
validation_hash=gen_data.get("validationHash"),
|
||||
prompts=prompts,
|
||||
# only set machine for machine-specific generators
|
||||
# this is essential for the graph algorithms to work correctly
|
||||
machine=None if share else machine_name,
|
||||
# shared generators can have multiple machines, machine-specific have one
|
||||
machines=[machine_name],
|
||||
_flake=flake,
|
||||
_public_store=pub_store,
|
||||
_secret_store=sec_store,
|
||||
)
|
||||
|
||||
generators.append(generator)
|
||||
if share:
|
||||
# For shared generators, check if we already created it
|
||||
existing = next(
|
||||
(g for g in generators if g.name == gen_name and g.share), None
|
||||
)
|
||||
if existing:
|
||||
# Just append the machine to the existing generator
|
||||
existing.machines.append(machine_name)
|
||||
else:
|
||||
# Add the new shared generator
|
||||
generators.append(generator)
|
||||
else:
|
||||
# Always add per-machine generators
|
||||
generators.append(generator)
|
||||
|
||||
# TODO: This should be done in a non-mutable way.
|
||||
if include_previous_values:
|
||||
|
||||
@@ -49,28 +49,28 @@ def test_required_generators() -> None:
|
||||
gen_1 = Generator(
|
||||
name="gen_1",
|
||||
dependencies=[],
|
||||
machine=machine_name,
|
||||
machines=[machine_name],
|
||||
_public_store=public_store,
|
||||
_secret_store=secret_store,
|
||||
)
|
||||
gen_2 = Generator(
|
||||
name="gen_2",
|
||||
dependencies=[gen_1.key],
|
||||
machine=machine_name,
|
||||
machines=[machine_name],
|
||||
_public_store=public_store,
|
||||
_secret_store=secret_store,
|
||||
)
|
||||
gen_2a = Generator(
|
||||
name="gen_2a",
|
||||
dependencies=[gen_2.key],
|
||||
machine=machine_name,
|
||||
machines=[machine_name],
|
||||
_public_store=public_store,
|
||||
_secret_store=secret_store,
|
||||
)
|
||||
gen_2b = Generator(
|
||||
name="gen_2b",
|
||||
dependencies=[gen_2.key],
|
||||
machine=machine_name,
|
||||
machines=[machine_name],
|
||||
_public_store=public_store,
|
||||
_secret_store=secret_store,
|
||||
)
|
||||
@@ -118,21 +118,22 @@ def test_shared_generator_invalidates_multiple_machines_dependents() -> None:
|
||||
shared_gen = Generator(
|
||||
name="shared_gen",
|
||||
dependencies=[],
|
||||
machine=None, # Shared generator
|
||||
share=True, # Mark as shared generator
|
||||
machines=[machine_1, machine_2], # Shared across both machines
|
||||
_public_store=public_store,
|
||||
_secret_store=secret_store,
|
||||
)
|
||||
gen_1 = Generator(
|
||||
name="gen_1",
|
||||
dependencies=[shared_gen.key],
|
||||
machine=machine_1,
|
||||
machines=[machine_1],
|
||||
_public_store=public_store,
|
||||
_secret_store=secret_store,
|
||||
)
|
||||
gen_2 = Generator(
|
||||
name="gen_2",
|
||||
dependencies=[shared_gen.key],
|
||||
machine=machine_2,
|
||||
machines=[machine_2],
|
||||
_public_store=public_store,
|
||||
_secret_store=secret_store,
|
||||
)
|
||||
|
||||
53
pkgs/clan-cli/clan_lib/persist/modules.nix
Normal file
53
pkgs/clan-cli/clan_lib/persist/modules.nix
Normal file
@@ -0,0 +1,53 @@
|
||||
let
|
||||
lib = import /home/johannes/git/nixpkgs/lib;
|
||||
|
||||
clanLib = import ../../../../lib { inherit lib; };
|
||||
|
||||
inherit (lib) evalModules mkOption types;
|
||||
|
||||
eval = evalModules {
|
||||
modules = [
|
||||
{
|
||||
options.foos = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule {
|
||||
options.bar = mkOption { };
|
||||
}
|
||||
);
|
||||
};
|
||||
# config.foos = lib.mkForce { this.bar = 42; };
|
||||
config.instances.a = { };
|
||||
# config.foo = lib.mkForce {
|
||||
# bar = 42;
|
||||
# };
|
||||
}
|
||||
{
|
||||
_file = "inventory.json";
|
||||
# instances.a = { setting = };
|
||||
}
|
||||
|
||||
# {
|
||||
# options.foo = mkOption {
|
||||
# type = types.attrsOf (types.attrsOf (types.submoduleWith { modules = [
|
||||
# {
|
||||
# options.bar = mkOption {};
|
||||
# }
|
||||
# ]; }));
|
||||
# default = { bar = { }; };
|
||||
# };
|
||||
# }
|
||||
# {
|
||||
# _file = "static.nix";
|
||||
# foo.static.thing = { bar = 1; }; # <- Can: Op.Modify
|
||||
# }
|
||||
# {
|
||||
# _file = "inventory.json";
|
||||
# foo.managed.thing = { bar = 1; }; # <- Can: Op.Delete, Op.Modify
|
||||
# #
|
||||
# }
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit clanLib eval;
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
from enum import Enum
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.persist.path_utils import PathTuple, path_to_string
|
||||
from clan_lib.persist.path_utils import (
|
||||
PathTuple,
|
||||
path_to_string,
|
||||
)
|
||||
|
||||
WRITABLE_PRIORITY_THRESHOLD = 100 # Values below this are not writeable
|
||||
|
||||
@@ -189,3 +193,69 @@ def compute_write_map(
|
||||
|
||||
"""
|
||||
return _determine_writeability_recursive(priorities, all_values, persisted)
|
||||
|
||||
|
||||
class RawAttributes(TypedDict):
|
||||
headType: str
|
||||
nullable: bool
|
||||
prio: int
|
||||
total: bool
|
||||
files: list[str]
|
||||
|
||||
|
||||
class OpType(Enum):
|
||||
MODIFY = "modify"
|
||||
DELETE = "delete"
|
||||
|
||||
|
||||
def transform_attribute_properties(
|
||||
introspection: dict[str, Any],
|
||||
all_values: dict[str, Any],
|
||||
persisted: dict[str, Any],
|
||||
# Passthrough for recursion
|
||||
curr_path: PathTuple = (),
|
||||
parent_attributes: RawAttributes | None = None,
|
||||
) -> dict[PathTuple, set[OpType]]:
|
||||
"""Transform attribute properties to ensure correct types and defaults."""
|
||||
results: dict[PathTuple, set[OpType]] = {}
|
||||
|
||||
for key, key_meta in introspection.items():
|
||||
if key in {"__this", "__list"}:
|
||||
continue
|
||||
|
||||
path = (*curr_path, key)
|
||||
results[path] = set()
|
||||
|
||||
local_attributes: RawAttributes = key_meta.get("__this")
|
||||
|
||||
key_priority = local_attributes["prio"] or None
|
||||
|
||||
effective_priority = key_priority or (
|
||||
parent_attributes["prio"] if parent_attributes else None
|
||||
)
|
||||
if effective_priority is None:
|
||||
msg = f"Priority for path '{path_to_string(path)}' is not defined and no parent to inherit from. Cannot determine effective priority."
|
||||
raise ClanError(msg)
|
||||
|
||||
if isinstance(key_meta, dict):
|
||||
subattrs = transform_attribute_properties(
|
||||
key_meta,
|
||||
all_values.get(key, {}),
|
||||
persisted.get(key, {}),
|
||||
curr_path=path,
|
||||
parent_attributes=local_attributes,
|
||||
)
|
||||
results.update(dict(subattrs.items()))
|
||||
|
||||
return results
|
||||
|
||||
# Only defined in inventory.json -> We might be able to delete it, because we defined it.
|
||||
# But we could also have some option default somewhere else, so we cannot be sure.
|
||||
# if all(f.endswith("inventory.json") for f in raw_attributes["files"]):
|
||||
# operations.add(OpType.DELETE)
|
||||
|
||||
# if (
|
||||
# raw_attributes["prio"] >= WRITABLE_PRIORITY_THRESHOLD
|
||||
# or ".json" in raw_attributes["files"]
|
||||
# ):
|
||||
# operations.add(OpType.MODIFY)
|
||||
|
||||
@@ -5,11 +5,110 @@ import pytest
|
||||
|
||||
from clan_lib.flake.flake import Flake
|
||||
from clan_lib.persist.inventory_store import InventoryStore
|
||||
from clan_lib.persist.write_rules import compute_write_map
|
||||
from clan_lib.persist.write_rules import (
|
||||
compute_write_map,
|
||||
transform_attribute_properties,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_lib.nix_models.clan import Clan
|
||||
|
||||
# foos.this = lib.mkForce { bar = 42; };
|
||||
# ->
|
||||
# {
|
||||
# foos = {
|
||||
# __this = {
|
||||
# files = [
|
||||
# "inventory.json"
|
||||
# "<unknown-file>"
|
||||
# ];
|
||||
# headType = "attrsOf";
|
||||
# nullable = false;
|
||||
# prio = 100;
|
||||
# total = false;
|
||||
# };
|
||||
# this = {
|
||||
# __this = {
|
||||
# files = [ "<unknown-file>" ];
|
||||
# headType = "submodule";
|
||||
# nullable = false;
|
||||
# prio = 50;
|
||||
# total = true;
|
||||
# };
|
||||
# bar = {
|
||||
# __this = {
|
||||
# files = [ "<unknown-file>" ];
|
||||
# headType = "unspecified";
|
||||
# nullable = false;
|
||||
# prio = 100;
|
||||
# total = false;
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
# }
|
||||
|
||||
|
||||
def test_write_new() -> None:
|
||||
all_data: dict = {"foo": {"bar": 42}}
|
||||
persisted_data: dict = {}
|
||||
introspection: dict = {
|
||||
"foo": {
|
||||
"__this": {
|
||||
"files": ["/dir/file.nix"],
|
||||
"headType": "unspecified",
|
||||
"nullable": False,
|
||||
"prio": 100, # <- default prio
|
||||
"total": False,
|
||||
},
|
||||
"bar": {
|
||||
"__this": {
|
||||
"files": ["/dir/file.nix"],
|
||||
"headType": "int",
|
||||
"nullable": False,
|
||||
"prio": 100, # <- default prio
|
||||
"total": False,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
res = transform_attribute_properties(introspection, all_data, persisted_data)
|
||||
|
||||
breakpoint()
|
||||
|
||||
# No operations allowed, because mkForce
|
||||
# We cannot modify this value in ANY possible way.
|
||||
# inventory.json definitions and children definition are filtered out by the module system
|
||||
# assert attributes == {"operations": set(), "path": ["foo", "bar"]}
|
||||
|
||||
# normal_prio_attrs: RawAttributes = {
|
||||
# "files": ["/dir/file.nix"],
|
||||
# "headType": "attrsOf",
|
||||
# "nullable": False,
|
||||
# "prio": 100, # <- default prio
|
||||
# "total": False,
|
||||
# }
|
||||
|
||||
# attributes = transform_attribute_properties(("foo", "bar"), normal_prio_attrs)
|
||||
|
||||
# # We can modify this value, because its a normal prio
|
||||
# # This means keys can be added/removed/changed respecting their individual local constraints
|
||||
# assert attributes == {"operations": { OpType.MODIFY }, "path": ["foo", "bar"]}
|
||||
|
||||
# default_prio_attrs: RawAttributes = {
|
||||
# "files": ["/dir/file.nix"],
|
||||
# "headType": "attrsOf",
|
||||
# "nullable": False,
|
||||
# "prio": 100, # <- default prio
|
||||
# "total": False,
|
||||
# }
|
||||
# attributes = transform_attribute_properties(("foo", "bar"), default_prio_attrs)
|
||||
|
||||
# # We can modify this value, because its a normal prio
|
||||
# # This means keys can be added/removed/changed respecting their individual local constraints
|
||||
# assert attributes == {"operations": { OpType.MODIFY, OpType.DELETE }, "path": ["foo", "bar"]}
|
||||
|
||||
|
||||
# Integration test
|
||||
@pytest.mark.with_core
|
||||
|
||||
@@ -93,21 +93,21 @@ def _ensure_healthy(
|
||||
if generators is None:
|
||||
generators = Generator.get_machine_generators([machine.name], machine.flake)
|
||||
|
||||
pub_healtcheck_msg = machine.public_vars_store.health_check(
|
||||
public_health_check_msg = machine.public_vars_store.health_check(
|
||||
machine.name,
|
||||
generators,
|
||||
)
|
||||
sec_healtcheck_msg = machine.secret_vars_store.health_check(
|
||||
secret_health_check_msg = machine.secret_vars_store.health_check(
|
||||
machine.name,
|
||||
generators,
|
||||
)
|
||||
|
||||
if pub_healtcheck_msg or sec_healtcheck_msg:
|
||||
if public_health_check_msg or secret_health_check_msg:
|
||||
msg = f"Health check failed for machine {machine.name}:\n"
|
||||
if pub_healtcheck_msg:
|
||||
msg += f"Public vars store: {pub_healtcheck_msg}\n"
|
||||
if sec_healtcheck_msg:
|
||||
msg += f"Secret vars store: {sec_healtcheck_msg}"
|
||||
if public_health_check_msg:
|
||||
msg += f"Public vars store: {public_health_check_msg}\n"
|
||||
if secret_health_check_msg:
|
||||
msg += f"Secret vars store: {secret_health_check_msg}"
|
||||
raise ClanError(msg)
|
||||
|
||||
|
||||
@@ -181,10 +181,10 @@ def run_generators(
|
||||
flake = machines[0].flake
|
||||
|
||||
def get_generator_machine(generator: Generator) -> Machine:
|
||||
if generator.machine is None:
|
||||
# return first machine if generator is not tied to a specific one
|
||||
if generator.share:
|
||||
# return first machine if generator is shared
|
||||
return machines[0]
|
||||
return Machine(name=generator.machine, flake=flake)
|
||||
return Machine(name=generator.machines[0], flake=flake)
|
||||
|
||||
# preheat the select cache, to reduce repeated calls during execution
|
||||
selectors = []
|
||||
|
||||
71
pkgs/docs-from-code/flake-module.nix
Normal file
71
pkgs/docs-from-code/flake-module.nix
Normal file
@@ -0,0 +1,71 @@
|
||||
{ self, inputs, ... }:
|
||||
{
|
||||
perSystem =
|
||||
{ pkgs, self', ... }:
|
||||
let
|
||||
# Simply evaluated options (JSON)
|
||||
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
|
||||
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
|
||||
inherit (self) clanModules;
|
||||
clan-core = self;
|
||||
inherit pkgs;
|
||||
};
|
||||
|
||||
# clan service options
|
||||
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
|
||||
|
||||
# Simply evaluated options (JSON)
|
||||
renderOptions =
|
||||
pkgs.runCommand "render-options"
|
||||
{
|
||||
# TODO: ruff does not splice properly in nativeBuildInputs
|
||||
depsBuildBuild = [ pkgs.ruff ];
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.mypy
|
||||
self'.packages.clan-cli
|
||||
];
|
||||
}
|
||||
''
|
||||
install -D -m755 ${./generate}/__init__.py $out/bin/render-options
|
||||
patchShebangs --build $out/bin/render-options
|
||||
|
||||
ruff format --check --diff $out/bin/render-options
|
||||
ruff check --line-length 88 $out/bin/render-options
|
||||
mypy --strict $out/bin/render-options
|
||||
'';
|
||||
|
||||
module-docs =
|
||||
pkgs.runCommand "rendered"
|
||||
{
|
||||
buildInputs = [
|
||||
pkgs.python3
|
||||
self'.packages.clan-cli
|
||||
];
|
||||
}
|
||||
''
|
||||
export CLAN_CORE_PATH=${
|
||||
inputs.nixpkgs.lib.fileset.toSource {
|
||||
root = ../..;
|
||||
fileset = ../../clanModules;
|
||||
}
|
||||
}
|
||||
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
||||
|
||||
# A file that contains the links to all clanModule docs
|
||||
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
|
||||
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
|
||||
export CLAN_OPTIONS_PATH=${self'.legacyPackages.clan-options}/share/doc/nixos/options.json
|
||||
|
||||
mkdir $out
|
||||
|
||||
# The python script will place mkDocs files in the output directory
|
||||
exec python3 ${renderOptions}/bin/render-options
|
||||
'';
|
||||
in
|
||||
{
|
||||
packages = {
|
||||
inherit module-docs;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
{
|
||||
imports = [
|
||||
./clan-cli/flake-module.nix
|
||||
./clan-vm-manager/flake-module.nix
|
||||
./installer/flake-module.nix
|
||||
./icon-update/flake-module.nix
|
||||
./clan-core-flake/flake-module.nix
|
||||
./clan-app/flake-module.nix
|
||||
./clan-cli/flake-module.nix
|
||||
./clan-core-flake/flake-module.nix
|
||||
./clan-vm-manager/flake-module.nix
|
||||
./icon-update/flake-module.nix
|
||||
./installer/flake-module.nix
|
||||
./option-search/flake-module.nix
|
||||
./docs-from-code/flake-module.nix
|
||||
./testing/flake-module.nix
|
||||
];
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
serviceModules = self.clan.modules;
|
||||
|
||||
baseHref = "/options/";
|
||||
baseHref = "/option-search/";
|
||||
|
||||
getRoles =
|
||||
module:
|
||||
@@ -118,7 +118,7 @@
|
||||
_file = "docs flake-module";
|
||||
imports = [
|
||||
{ _module.args = { inherit clanLib; }; }
|
||||
(import ../../../lib/modules/inventoryClass/roles-interface.nix {
|
||||
(import ../../lib/modules/inventoryClass/roles-interface.nix {
|
||||
nestedSettingsOption = mkOption {
|
||||
type = types.raw;
|
||||
description = ''
|
||||
@@ -201,7 +201,7 @@
|
||||
# };
|
||||
|
||||
packages = {
|
||||
docs-options =
|
||||
option-search =
|
||||
if privateInputs ? nuschtos then
|
||||
privateInputs.nuschtos.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch {
|
||||
inherit baseHref;
|
||||
Reference in New Issue
Block a user