clan-config: add readme
Some checks failed
build / test (pull_request) Failing after 24s

Also add capability of reading an option value
This commit is contained in:
DavHau 2023-08-23 00:59:13 +02:00
parent 7b2e9cc46e
commit 999fbe0d89
6 changed files with 154 additions and 51 deletions

View File

@ -1,6 +1,7 @@
# clan.lol core # clan.lol core
This is the monorepo of the clan.lol project This is the monorepo of the clan.lol project
In here are all the packages we use, all the nixosModules we use/expose, the CLI and tests for everything. In here are all the packages we use, all the nixosModules we use/expose, the CLI and tests for everything.
Not in here is the deployed infrastructure, which is in clan-infra.
## cLAN config tool
Find the docs [here](/docs/clan-config.md)

65
docs/clan-config.md Normal file
View File

@ -0,0 +1,65 @@
# cLAN config
`clan config` allows you to manage your nixos configuration via the terminal.
Similar as how `git config` reads and sets git options, `clan config` does the same with your nixos options
It also supports auto completion making it easy to find the right options.
## Set up clan-config
Add the clan tool to your flake inputs:
```
clan.url = "git+https://git.clan.lol/clan/clan-core";
```
and inside the mkFlake:
```
imports = [
inputs.clan.flakeModules.clan-config
];
```
Add an empty config file and add it to git
```command
echo "{}" > ./clan-settings.json
git add ./clan-settings.json
```
Import the clan-config module into your nixos configuration:
```nix
{
imports = [
# clan-settings.json is located in the same directory as your flake.
# Adapt the path if necessary.
(builtins.fromJSON (builtins.readFile ./clan-settings.json))
];
}
```
Make sure your nixos configuration is set a default
```nix
{self, ...}: {
flake.nixosConfigurations.default = self.nixosConfigurations.my-machine;
}
```
Use all inputs provided by the clan-config devShell in your own devShell:
```nix
{ ... }: {
perSystem = { pkgs, self', ... }: {
devShells.default = pkgs.mkShell {
inputsFrom = [ self'.devShells.clan-config ];
# ...
};
};
}
```
re-load your dev-shell to make the clan tool available.
```command
clan config --help
```

View File

@ -27,11 +27,12 @@
./formatter.nix ./formatter.nix
./templates/flake-module.nix ./templates/flake-module.nix
./flakeModules/clan-config.nix
./pkgs/flake-module.nix ./pkgs/flake-module.nix
./lib/flake-module.nix ./lib/flake-module.nix
({ self, lib, ... }: { ({ self, lib, ... }: {
flake.flakeModules = lib.mapAttrs (_: nix: { imports = [ nix ]; }) (self.lib.findNixFiles ./flakeModules);
flake.clanModules = lib.mapAttrs (_: nix: { imports = [ nix ]; }) (self.lib.findNixFiles ./clanModules); flake.clanModules = lib.mapAttrs (_: nix: { imports = [ nix ]; }) (self.lib.findNixFiles ./clanModules);
flake.nixosModules = lib.mapAttrs (_: nix: { imports = [ nix ]; }) (self.lib.findNixFiles ./nixosModules); flake.nixosModules = lib.mapAttrs (_: nix: { imports = [ nix ]; }) (self.lib.findNixFiles ./nixosModules);
}) })

View File

@ -1,40 +1,42 @@
{ self, inputs, ... }: { ... } @ clanCore: {
let flake.flakeModules.clan-config = { self, inputs, ... }:
let
# take the default nixos configuration # take the default nixos configuration
options = self.nixosConfigurations.default.options; options = self.nixosConfigurations.default.options;
# this is actually system independent as it uses toFile # this is actually system independent as it uses toFile
docs = inputs.nixpkgs.legacyPackages.x86_64-linux.nixosOptionsDoc { docs = inputs.nixpkgs.legacyPackages.x86_64-linux.nixosOptionsDoc {
inherit options; inherit options;
}; };
optionsJSONFile = docs.optionsJSON.options; optionsJSONFile = docs.optionsJSON.options;
warnIfNoDefaultConfig = return: warnIfNoDefaultConfig = return:
if ! self ? nixosConfigurations.default if ! self ? nixosConfigurations.default
then then
builtins.trace builtins.trace
"WARNING: .#nixosConfigurations.default could not be found. Please define it." "WARNING: .#nixosConfigurations.default could not be found. Please define it."
return return
else return; else return;
in in
{ {
flake.clanOptions = warnIfNoDefaultConfig optionsJSONFile; flake.clanOptions = warnIfNoDefaultConfig optionsJSONFile;
flake.clanSettings = self + /clan-settings.json; flake.clanSettings = self + /clan-settings.json;
perSystem = { pkgs, inputs', ... }: { perSystem = { pkgs, ... }: {
devShells.clan-config = pkgs.mkShell { devShells.clan-config = pkgs.mkShell {
packages = [ packages = [
inputs'.clan-core.packages.clan-cli clanCore.config.flake.packages.${pkgs.system}.clan-cli
]; ];
shellHook = '' shellHook = ''
export CLAN_OPTIONS_FILE=$(nix eval --raw .#clanOptions) export CLAN_OPTIONS_FILE=$(nix eval --raw .#clanOptions)
export XDG_DATA_DIRS="${inputs'.clan-core.packages.clan-cli}/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" export XDG_DATA_DIRS="${clanCore.config.flake.packages.${pkgs.system}.clan-cli}/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export fish_complete_path="${inputs'.clan-core.packages.clan-cli}/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" export fish_complete_path="${clanCore.config.flake.packages.${pkgs.system}.clan-cli}/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}"
''; '';
};
};
}; };
};
} }

View File

@ -43,7 +43,7 @@ def merge(a: dict, b: dict, path: list[str] = []) -> dict:
elif isinstance(a[key], list) and isinstance(b[key], list): elif isinstance(a[key], list) and isinstance(b[key], list):
a[key].extend(b[key]) a[key].extend(b[key])
elif a[key] != b[key]: elif a[key] != b[key]:
raise Exception("Conflict at " + ".".join(path + [str(key)])) a[key] = b[key]
else: else:
a[key] = b[key] a[key] = b[key]
return a return a
@ -91,13 +91,49 @@ def cast(value: Any, type: Type, opt_description: str) -> Any:
) )
def read_option(option: str) -> str:
# use nix eval to read from .#nixosConfigurations.default.config.{option}
# this will give us the evaluated config with the options attribute
proc = subprocess.run(
[
"nix",
"eval",
"--json",
f".#nixosConfigurations.default.config.{option}",
],
capture_output=True,
text=True,
)
if proc.returncode != 0:
print(proc.stderr, file=sys.stderr)
raise ClanError(f"Failed to read option {option}:\n{proc.stderr}")
value = json.loads(proc.stdout)
# print the value so that the output can be copied and fed as an input.
# for example a list should be displayed as space separated values surrounded by quotes.
if isinstance(value, list):
out = " ".join([json.dumps(x) for x in value])
elif isinstance(value, dict):
out = json.dumps(value, indent=2)
else:
out = json.dumps(value, indent=2)
return out
def process_args( def process_args(
option: str, value: Any, options: dict, out_file: Path, option_description: str = "" option: str,
value: Any,
options: dict,
settings_file: Path,
option_description: str = "",
) -> None: ) -> None:
if value == []:
print(read_option(option))
return
option_path = option.split(".") option_path = option.split(".")
# if the option cannot be found, then likely the type is attrs and we need to # if the option cannot be found, then likely the type is attrs and we need to
# find the parent option # find the parent option.
if option not in options: if option not in options:
if len(option_path) == 1: if len(option_path) == 1:
raise ClanError(f"Option {option_description} not found") raise ClanError(f"Option {option_description} not found")
@ -107,11 +143,12 @@ def process_args(
option=".".join(option_parent), option=".".join(option_parent),
value={attr: value}, value={attr: value},
options=options, options=options,
out_file=out_file, settings_file=settings_file,
option_description=option, option_description=option,
) )
target_type = map_type(options[option]["type"]) target_type = map_type(options[option]["type"])
casted = cast(value, target_type, option)
# construct a nested dict from the option path and set the value # construct a nested dict from the option path and set the value
result: dict[str, Any] = {} result: dict[str, Any] = {}
@ -121,22 +158,21 @@ def process_args(
current = current[part] current = current[part]
current[option_path[-1]] = value current[option_path[-1]] = value
casted = cast(value, target_type, option)
current[option_path[-1]] = casted current[option_path[-1]] = casted
# check if there is an existing config file # check if there is an existing config file
if os.path.exists(out_file): if os.path.exists(settings_file):
with open(out_file) as f: with open(settings_file) as f:
current_config = json.load(f) current_config = json.load(f)
else: else:
current_config = {} current_config = {}
# merge and save the new config file # merge and save the new config file
new_config = merge(current_config, result) new_config = merge(current_config, result)
with open(out_file, "w") as f: with open(settings_file, "w") as f:
json.dump(new_config, f, indent=2) json.dump(new_config, f, indent=2)
print("New config:") new_value = read_option(option)
print(json.dumps(new_config, indent=2)) print(f"New Value for {option}:")
print(new_value)
def register_parser( def register_parser(
@ -182,13 +218,13 @@ def _register_parser(
option=args.option, option=args.option,
value=args.value, value=args.value,
options=options, options=options,
out_file=args.out_file, settings_file=args.settings_file,
) )
) )
# add argument to pass output file # add argument to pass output file
parser.add_argument( parser.add_argument(
"--out-file", "--settings-file",
"-o", "-o",
help="Output file", help="Output file",
type=Path, type=Path,
@ -198,8 +234,6 @@ def _register_parser(
# add single positional argument for the option (e.g. "foo.bar") # add single positional argument for the option (e.g. "foo.bar")
parser.add_argument( parser.add_argument(
"option", "option",
# force this arg to be set
nargs="?",
help="Option to configure", help="Option to configure",
type=str, type=str,
choices=AllContainer(list(options.keys())), choices=AllContainer(list(options.keys())),
@ -209,7 +243,7 @@ def _register_parser(
parser.add_argument( parser.add_argument(
"value", "value",
# force this arg to be set # force this arg to be set
nargs="+", nargs="*",
help="Value to set", help="Value to set",
) )

View File

@ -37,7 +37,7 @@ def test_set_some_option(
with tempfile.NamedTemporaryFile() as out_file: with tempfile.NamedTemporaryFile() as out_file:
with open(out_file.name, "w") as f: with open(out_file.name, "w") as f:
json.dump({}, f) json.dump({}, f)
monkeypatch.setattr(sys, "argv", ["", "--out-file", out_file.name] + argv) monkeypatch.setattr(sys, "argv", ["", "--settings-file", out_file.name] + argv)
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
config._register_parser( config._register_parser(
parser=parser, parser=parser,