From 999fbe0d892eb0f62ae6c4b7fd05993ba597b592 Mon Sep 17 00:00:00 2001 From: DavHau Date: Wed, 23 Aug 2023 00:59:13 +0200 Subject: [PATCH] clan-config: add readme Also add capability of reading an option value --- README.md | 5 +- docs/clan-config.md | 65 ++++++++++++++++++++++ flake.nix | 3 +- flakeModules/clan-config.nix | 64 +++++++++++----------- pkgs/clan-cli/clan_cli/config/__init__.py | 66 +++++++++++++++++------ pkgs/clan-cli/tests/test_config.py | 2 +- 6 files changed, 154 insertions(+), 51 deletions(-) create mode 100644 docs/clan-config.md diff --git a/README.md b/README.md index 612b05da..d58f1488 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # clan.lol core 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. -Not in here is the deployed infrastructure, which is in clan-infra. \ No newline at end of file + +## cLAN config tool +Find the docs [here](/docs/clan-config.md) diff --git a/docs/clan-config.md b/docs/clan-config.md new file mode 100644 index 00000000..3855cf11 --- /dev/null +++ b/docs/clan-config.md @@ -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 +``` diff --git a/flake.nix b/flake.nix index 3d47cb4d..d290e0cb 100644 --- a/flake.nix +++ b/flake.nix @@ -27,11 +27,12 @@ ./formatter.nix ./templates/flake-module.nix + ./flakeModules/clan-config.nix + ./pkgs/flake-module.nix ./lib/flake-module.nix ({ 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.nixosModules = lib.mapAttrs (_: nix: { imports = [ nix ]; }) (self.lib.findNixFiles ./nixosModules); }) diff --git a/flakeModules/clan-config.nix b/flakeModules/clan-config.nix index 8f6ee983..236d2259 100644 --- a/flakeModules/clan-config.nix +++ b/flakeModules/clan-config.nix @@ -1,40 +1,42 @@ -{ self, inputs, ... }: -let +{ ... } @ clanCore: { + flake.flakeModules.clan-config = { self, inputs, ... }: + let - # take the default nixos configuration - options = self.nixosConfigurations.default.options; + # take the default nixos configuration + options = self.nixosConfigurations.default.options; - # this is actually system independent as it uses toFile - docs = inputs.nixpkgs.legacyPackages.x86_64-linux.nixosOptionsDoc { - inherit options; - }; + # this is actually system independent as it uses toFile + docs = inputs.nixpkgs.legacyPackages.x86_64-linux.nixosOptionsDoc { + inherit options; + }; - optionsJSONFile = docs.optionsJSON.options; + optionsJSONFile = docs.optionsJSON.options; - warnIfNoDefaultConfig = return: - if ! self ? nixosConfigurations.default - then - builtins.trace - "WARNING: .#nixosConfigurations.default could not be found. Please define it." - return - else return; + warnIfNoDefaultConfig = return: + if ! self ? nixosConfigurations.default + then + builtins.trace + "WARNING: .#nixosConfigurations.default could not be found. Please define it." + return + else return; -in -{ - flake.clanOptions = warnIfNoDefaultConfig optionsJSONFile; + in + { + flake.clanOptions = warnIfNoDefaultConfig optionsJSONFile; - flake.clanSettings = self + /clan-settings.json; + flake.clanSettings = self + /clan-settings.json; - perSystem = { pkgs, inputs', ... }: { - devShells.clan-config = pkgs.mkShell { - packages = [ - inputs'.clan-core.packages.clan-cli - ]; - shellHook = '' - 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 fish_complete_path="${inputs'.clan-core.packages.clan-cli}/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" - ''; + perSystem = { pkgs, ... }: { + devShells.clan-config = pkgs.mkShell { + packages = [ + clanCore.config.flake.packages.${pkgs.system}.clan-cli + ]; + shellHook = '' + export CLAN_OPTIONS_FILE=$(nix eval --raw .#clanOptions) + export XDG_DATA_DIRS="${clanCore.config.flake.packages.${pkgs.system}.clan-cli}/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" + export fish_complete_path="${clanCore.config.flake.packages.${pkgs.system}.clan-cli}/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" + ''; + }; + }; }; - }; } diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index 3dcbc93f..fee0f332 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -43,7 +43,7 @@ def merge(a: dict, b: dict, path: list[str] = []) -> dict: elif isinstance(a[key], list) and isinstance(b[key], list): a[key].extend(b[key]) elif a[key] != b[key]: - raise Exception("Conflict at " + ".".join(path + [str(key)])) + a[key] = b[key] else: a[key] = b[key] 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( - 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: + if value == []: + print(read_option(option)) + return + option_path = option.split(".") # 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 len(option_path) == 1: raise ClanError(f"Option {option_description} not found") @@ -107,11 +143,12 @@ def process_args( option=".".join(option_parent), value={attr: value}, options=options, - out_file=out_file, + settings_file=settings_file, option_description=option, ) 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 result: dict[str, Any] = {} @@ -121,22 +158,21 @@ def process_args( current = current[part] current[option_path[-1]] = value - casted = cast(value, target_type, option) - current[option_path[-1]] = casted # check if there is an existing config file - if os.path.exists(out_file): - with open(out_file) as f: + if os.path.exists(settings_file): + with open(settings_file) as f: current_config = json.load(f) else: current_config = {} # merge and save the new config file 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) - print("New config:") - print(json.dumps(new_config, indent=2)) + new_value = read_option(option) + print(f"New Value for {option}:") + print(new_value) def register_parser( @@ -182,13 +218,13 @@ def _register_parser( option=args.option, value=args.value, options=options, - out_file=args.out_file, + settings_file=args.settings_file, ) ) # add argument to pass output file parser.add_argument( - "--out-file", + "--settings-file", "-o", help="Output file", type=Path, @@ -198,8 +234,6 @@ def _register_parser( # add single positional argument for the option (e.g. "foo.bar") parser.add_argument( "option", - # force this arg to be set - nargs="?", help="Option to configure", type=str, choices=AllContainer(list(options.keys())), @@ -209,7 +243,7 @@ def _register_parser( parser.add_argument( "value", # force this arg to be set - nargs="+", + nargs="*", help="Value to set", ) diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/test_config.py index effba091..ab4e6349 100644 --- a/pkgs/clan-cli/tests/test_config.py +++ b/pkgs/clan-cli/tests/test_config.py @@ -37,7 +37,7 @@ def test_set_some_option( with tempfile.NamedTemporaryFile() as out_file: with open(out_file.name, "w") as 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() config._register_parser( parser=parser,