Also add capability of reading an option value
This commit is contained in:
parent
7b2e9cc46e
commit
999fbe0d89
|
@ -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
65
docs/clan-config.md
Normal 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
|
||||||
|
```
|
|
@ -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);
|
||||||
})
|
})
|
||||||
|
|
|
@ -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}"
|
||||||
'';
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user