clan-config: init
- nixos-modules to jsonschema converter - nix unit testing via adisbladis/nix-unit - clan config: configuration CLI for nixos-modules
This commit is contained in:
@@ -18,22 +18,6 @@ def create(args: argparse.Namespace) -> None:
|
||||
)
|
||||
|
||||
|
||||
def edit(args: argparse.Namespace) -> None:
|
||||
# TODO add some cli options to change certain options without relying on a text editor
|
||||
clan_flake = f"{args.folder}/flake.nix"
|
||||
if os.path.isfile(clan_flake):
|
||||
subprocess.Popen(
|
||||
[
|
||||
os.environ["EDITOR"],
|
||||
clan_flake,
|
||||
]
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"{args.folder} has no flake.nix, so it does not seem to be the clan root folder",
|
||||
)
|
||||
|
||||
|
||||
def rebuild(args: argparse.Namespace) -> None:
|
||||
# TODO get clients from zerotier cli?
|
||||
if args.host:
|
||||
@@ -89,9 +73,6 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser_create = subparser.add_parser("create", help="create a new clan")
|
||||
parser_create.set_defaults(func=create)
|
||||
|
||||
parser_edit = subparser.add_parser("edit", help="edit a clan")
|
||||
parser_edit.set_defaults(func=edit)
|
||||
|
||||
parser_rebuild = subparser.add_parser(
|
||||
"rebuild", help="build configuration of a clan and push it to the target"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from . import admin, secrets, ssh
|
||||
from . import admin, config, secrets, ssh
|
||||
from .errors import ClanError
|
||||
|
||||
has_argcomplete = True
|
||||
@@ -19,6 +19,9 @@ def main() -> None:
|
||||
parser_admin = subparsers.add_parser("admin")
|
||||
admin.register_parser(parser_admin)
|
||||
|
||||
parser_config = subparsers.add_parser("config")
|
||||
config.register_parser(parser_config)
|
||||
|
||||
parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine")
|
||||
ssh.register_parser(parser_ssh)
|
||||
|
||||
|
||||
99
pkgs/clan-cli/clan_cli/config/__init__.py
Normal file
99
pkgs/clan-cli/clan_cli/config/__init__.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# !/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
|
||||
class Kwargs:
|
||||
def __init__(self):
|
||||
self.type = None
|
||||
self.default: Any = None
|
||||
self.required: bool = False
|
||||
self.help: Optional[str] = None
|
||||
self.action: Optional[str] = None
|
||||
self.choices: Optional[list] = None
|
||||
|
||||
|
||||
# takes a (sub)parser and configures it
|
||||
def register_parser(
|
||||
parser: Optional[argparse.ArgumentParser] = None,
|
||||
schema: Union[dict, str, Path] = "./tests/config/example-schema.json",
|
||||
) -> dict:
|
||||
if not isinstance(schema, dict):
|
||||
with open(str(schema)) as f:
|
||||
schema: dict = json.load(f)
|
||||
assert "type" in schema and schema["type"] == "object"
|
||||
|
||||
required_set = set(schema.get("required", []))
|
||||
|
||||
if parser is None:
|
||||
parser = argparse.ArgumentParser(description=schema.get("description"))
|
||||
|
||||
type_map = {
|
||||
"array": list,
|
||||
"boolean": bool,
|
||||
"integer": int,
|
||||
"number": float,
|
||||
"string": str,
|
||||
}
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
title="more options",
|
||||
description="Other options to configure",
|
||||
help="the option to configure",
|
||||
required=True,
|
||||
)
|
||||
|
||||
for name, value in schema.get("properties", {}).items():
|
||||
assert isinstance(value, dict)
|
||||
|
||||
# TODO: add support for nested objects
|
||||
if value.get("type") == "object":
|
||||
subparser = subparsers.add_parser(name, help=value.get("description"))
|
||||
register_parser(parser=subparser, schema=value)
|
||||
continue
|
||||
# elif value.get("type") == "array":
|
||||
# subparser = parser.add_subparsers(dest=name)
|
||||
# register_parser(subparser, value)
|
||||
# continue
|
||||
kwargs = Kwargs()
|
||||
kwargs.default = value.get("default")
|
||||
kwargs.help = value.get("description")
|
||||
kwargs.required = name in required_set
|
||||
|
||||
if kwargs.default is not None:
|
||||
kwargs.help = f"{kwargs.help}, [{kwargs.default}] in default"
|
||||
|
||||
if "enum" in value:
|
||||
enum_list = value["enum"]
|
||||
assert len(enum_list) > 0, "Enum List is Empty"
|
||||
arg_type = type(enum_list[0])
|
||||
assert all(
|
||||
arg_type is type(item) for item in enum_list
|
||||
), f"Items in [{enum_list}] with Different Types"
|
||||
|
||||
kwargs.type = arg_type
|
||||
kwargs.choices = enum_list
|
||||
else:
|
||||
kwargs.type = type_map[value.get("type")]
|
||||
del kwargs.choices
|
||||
|
||||
name = f"--{name}"
|
||||
|
||||
if kwargs.type is bool:
|
||||
assert not kwargs.default, "boolean have to be False in default"
|
||||
kwargs.default = False
|
||||
kwargs.action = "store_true"
|
||||
del kwargs.type
|
||||
else:
|
||||
del kwargs.action
|
||||
|
||||
parser.add_argument(name, **vars(kwargs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
register_parser(parser)
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
144
pkgs/clan-cli/clan_cli/config/schema-lib.nix
Normal file
144
pkgs/clan-cli/clan_cli/config/schema-lib.nix
Normal file
@@ -0,0 +1,144 @@
|
||||
{ lib ? (import <nixpkgs> { }).lib }:
|
||||
let
|
||||
|
||||
# from nixos type to jsonschema type
|
||||
typeMap = {
|
||||
bool = "boolean";
|
||||
float = "number";
|
||||
int = "integer";
|
||||
str = "string";
|
||||
};
|
||||
|
||||
# remove _module attribute from options
|
||||
clean = opts: builtins.removeAttrs opts [ "_module" ];
|
||||
|
||||
# throw error if option type is not supported
|
||||
notSupported = option: throw
|
||||
"option type '${option.type.description}' not supported by jsonschema converter";
|
||||
|
||||
in
|
||||
rec {
|
||||
|
||||
# parses a set of evaluated nixos options to a jsonschema
|
||||
parseOptions = options':
|
||||
let
|
||||
options = clean options';
|
||||
# parse options to jsonschema properties
|
||||
properties = lib.mapAttrs (_name: option: parseOption option) options;
|
||||
isRequired = prop: ! (prop ? default || prop.type == "object");
|
||||
requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties;
|
||||
required = lib.optionalAttrs (requiredProps != { }) {
|
||||
required = lib.attrNames requiredProps;
|
||||
};
|
||||
in
|
||||
# return jsonschema
|
||||
required // {
|
||||
type = "object";
|
||||
inherit properties;
|
||||
};
|
||||
|
||||
# parses and evaluated nixos option to a jsonschema property definition
|
||||
parseOption = option:
|
||||
let
|
||||
default = lib.optionalAttrs (option ? default) {
|
||||
inherit (option) default;
|
||||
};
|
||||
description = lib.optionalAttrs (option ? description) {
|
||||
inherit (option) description;
|
||||
};
|
||||
in
|
||||
if option._type != "option"
|
||||
then throw "parseOption: not an option"
|
||||
|
||||
# parse nullOr
|
||||
else if option.type.name == "nullOr"
|
||||
# return jsonschema property definition for nullOr
|
||||
then default // description // {
|
||||
type = [
|
||||
"null"
|
||||
(typeMap.${option.type.functor.wrapped.name} or (notSupported option))
|
||||
];
|
||||
}
|
||||
|
||||
# parse bool
|
||||
else if option.type.name == "bool"
|
||||
# return jsonschema property definition for bool
|
||||
then default // description // {
|
||||
type = "boolean";
|
||||
}
|
||||
|
||||
# parse float
|
||||
else if option.type.name == "float"
|
||||
# return jsonschema property definition for float
|
||||
then default // description // {
|
||||
type = "number";
|
||||
}
|
||||
|
||||
# parse int
|
||||
else if option.type.name == "int"
|
||||
# return jsonschema property definition for int
|
||||
then default // description // {
|
||||
type = "integer";
|
||||
}
|
||||
|
||||
# parse string
|
||||
else if option.type.name == "str"
|
||||
# return jsonschema property definition for string
|
||||
then default // description // {
|
||||
type = "string";
|
||||
}
|
||||
|
||||
# parse enum
|
||||
else if option.type.name == "enum"
|
||||
# return jsonschema property definition for enum
|
||||
then default // description // {
|
||||
enum = option.type.functor.payload;
|
||||
}
|
||||
|
||||
# parse listOf submodule
|
||||
else if option.type.name == "listOf" && option.type.functor.wrapped.name == "submodule"
|
||||
# return jsonschema property definition for listOf submodule
|
||||
then default // description // {
|
||||
type = "array";
|
||||
items = parseOptions (option.type.functor.wrapped.getSubOptions option.loc);
|
||||
}
|
||||
|
||||
# parse list
|
||||
else if
|
||||
(option.type.name == "listOf")
|
||||
&& (typeMap ? "${option.type.functor.wrapped.name}")
|
||||
# return jsonschema property definition for list
|
||||
then default // description // {
|
||||
type = "array";
|
||||
items = {
|
||||
type = typeMap.${option.type.functor.wrapped.name};
|
||||
};
|
||||
}
|
||||
|
||||
# parse attrsOf submodule
|
||||
else if option.type.name == "attrsOf" && option.type.nestedTypes.elemType.name == "submodule"
|
||||
# return jsonschema property definition for attrsOf submodule
|
||||
then default // description // {
|
||||
type = "object";
|
||||
additionalProperties = parseOptions (option.type.nestedTypes.elemType.getSubOptions option.loc);
|
||||
}
|
||||
|
||||
# parse attrs
|
||||
else if option.type.name == "attrsOf"
|
||||
# return jsonschema property definition for attrs
|
||||
then default // description // {
|
||||
type = "object";
|
||||
additionalProperties = {
|
||||
type = typeMap.${option.type.nestedTypes.elemType.name} or (notSupported option);
|
||||
};
|
||||
}
|
||||
|
||||
# parse submodule
|
||||
else if option.type.name == "submodule"
|
||||
# return jsonschema property definition for submodule
|
||||
# then (lib.attrNames (option.type.getSubOptions option.loc).opt)
|
||||
then parseOptions (option.type.getSubOptions option.loc)
|
||||
|
||||
# throw error if option type is not supported
|
||||
else notSupported option;
|
||||
}
|
||||
Reference in New Issue
Block a user