From a7772aa1545c3eb5e47274e3e5a6eb42192e1208 Mon Sep 17 00:00:00 2001 From: June Date: Mon, 10 Jun 2024 02:08:23 +0200 Subject: [PATCH] feature!: introduce --flake option for setting the flake to use Having this option available allows for using a flake which isn't in (or upwards of) the directory the command gets executed in and allows for using remote flakes. Also archive the flake to use first and then operate on the archive. This allows for easily getting the deployment_configuration.json from the archive and also ensures that once the archiving suceeds, there shouldn't be issues with the flakes source anymore. Since now the deployment_configuration.json will always be taken from the root of the flakes archive and therefore from the root of the flakes repo, this is a breaking change, since previously it was taken from the current working directory. The idea of archiving the flake first and operating on the archive comes from bij: https://git.clerie.de/clerie/bij/commit/221052d8465f0a4437cb8cae3cc9998c87e88f68 Moreover introduce helper functions for facilitating recursive options (i.e. options one can set on root and sub-commands). --- README.md | 2 +- src/infra_rebuild/cli/__init__.py | 86 +++++++++++++++++++----- src/infra_rebuild/operations/__init__.py | 38 ++++++++--- 3 files changed, 100 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index c1faf59..0d21df2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ However to override aspects of the target host for specific or all hosts, infra- ## Configuration -infra-rebuild accepts optional configuration in a `deployment_configuration.json`. \ +infra-rebuild accepts optional configuration in a `deployment_configuration.json` present in the flake repos root. \ The following keys are available to be set for configuring various aspects of deployment for specific or all hosts: - `default.targetPort`: A default port to use for connecting to all host. diff --git a/src/infra_rebuild/cli/__init__.py b/src/infra_rebuild/cli/__init__.py index 05cef25..fe31d77 100644 --- a/src/infra_rebuild/cli/__init__.py +++ b/src/infra_rebuild/cli/__init__.py @@ -3,54 +3,108 @@ import click from infra_rebuild import operations reboot_option_help_text = "Reboot the target hosts after running the operation." +flake_option_help_text = "URI of the flake to use. Defaults to the current direcory." + + +def initialize_recursive_option(ctx, option_name, default_value): + """Helper function for initializing a recursive option. + + Arguments: + `ctx` -- The click context passed to the calling command. + `option_name` -- The name of the option in question. + `default_value` -- A default value for the option. + + Simply sets the value of the recursive option (`ctx.obj[option_name]`) to `default_value`. + """ + ctx.obj[option_name] = default_value + + +def update_recursive_option(ctx, option_name, given_value): + """Helper function for updating a recursive option. + + Arguments: + ctx -- The click context passed to the calling command. + option_name -- The name of the option in question. + given_value -- The value the calling command received for the option. + + It overrides the current value for the recursive option (`ctx.obj[option_name]`) with `given_value`, if `given_value` isn't `None`. + This ensure precedence of the innermost command (since the innermost command would call this function last). + """ # noqa: E501 + if given_value is not None: + ctx.obj[option_name] = given_value @click.group(context_settings={"help_option_names": ["-h", "--help"]}, invoke_without_command=True) @click.version_option(prog_name="infra-rebuild") -def infra_rebuild(): - pass +@click.pass_context +@click.option("--flake", help=flake_option_help_text) +def infra_rebuild(ctx, flake): + ctx.ensure_object(dict) + initialize_recursive_option(ctx, "flake", ".") + update_recursive_option(ctx, "flake", flake) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.argument("hosts", nargs=-1) -def build(hosts): - operations.run("build", hosts) +def build(ctx, flake, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("build", hosts, ctx.obj["flake"]) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.argument("hosts", nargs=-1) -def build_vm(hosts): - operations.run("build-vm", hosts) +def build_vm(ctx, flake, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("build-vm", hosts, ctx.obj["flake"]) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.argument("hosts", nargs=-1) -def build_vm_with_bootloader(hosts): - operations.run("build-vm-with-bootloader", hosts) +def build_vm_with_bootloader(ctx, flake, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("build-vm-with-bootloader", hosts, ctx.obj["flake"]) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.option("--reboot", is_flag=True, help=reboot_option_help_text) @click.argument("hosts", nargs=-1) -def switch(hosts, reboot): - operations.run("switch", hosts, reboot) +def switch(ctx, flake, reboot, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("switch", hosts, ctx.obj["flake"], reboot) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.option("--reboot", is_flag=True, help=reboot_option_help_text) @click.argument("hosts", nargs=-1) -def boot(hosts, reboot): - operations.run("boot", hosts, reboot) +def boot(ctx, flake, reboot, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("boot", hosts, ctx.obj["flake"], reboot) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.option("--reboot", is_flag=True, help=reboot_option_help_text) @click.argument("hosts", nargs=-1) -def test(hosts, reboot): - operations.run("test", hosts, reboot) +def test(ctx, flake, reboot, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("test", hosts, ctx.obj["flake"], reboot) @infra_rebuild.command(short_help=" ", help="This operation is a convenience alias for boot --reboot.") +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.argument("hosts", nargs=-1) -def reboot(hosts): - operations.run("boot", hosts, True) +def reboot(ctx, flake, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("boot", hosts, ctx.obj["flake"], True) diff --git a/src/infra_rebuild/operations/__init__.py b/src/infra_rebuild/operations/__init__.py index b8d3f28..5368848 100644 --- a/src/infra_rebuild/operations/__init__.py +++ b/src/infra_rebuild/operations/__init__.py @@ -6,7 +6,27 @@ import sys from infra_rebuild import msg -def run(operation, hosts, reboot=False): # noqa: FBT002 - having reboot as a Boolean positional argument is fine I think # fmt: skip +def run(operation, hosts, flake_uri, reboot=False): # noqa: FBT002 - having reboot as a Boolean positional argument is fine I think # fmt: skip + try: + flake_archive_json = subprocess.run( + ["nix", "flake", "archive", "--json", f"{flake_uri}"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for the flake_uri in the future? + capture_output=True, + text=True, + check=True + ) # fmt: skip + try: + flake_archive = json.loads(flake_archive_json.stdout) + except json.JSONDecodeError: + msg.error("Internal Error: Couldn't parse the output of \"nix flake archive\".") + sys.exit(1) + + if not flake_archive["path"]: + msg.error("Internal Error: The \"nix flake archive\" output doesn't include a path.") + sys.exit(1) + except subprocess.CalledProcessError: + msg.error("Error: Couldn't archive the provided flake.") + sys.exit(1) + match operation: case "build" | "build-vm" | "build-vm-with-bootloader": act_remotely = False @@ -22,23 +42,23 @@ def run(operation, hosts, reboot=False): # noqa: FBT002 - having reboot as a Bo continue if act_remotely: - remote(operation, host, reboot) + remote(operation, host, flake_archive, reboot) else: - local(operation, host) + local(operation, host, flake_archive) -def local(operation, host): +def local(operation, host, flake_archive): msg.info(f"Running nixos-rebuild {operation} for {host}...") subprocess.run( - ["nixos-rebuild", operation, "--flake", f".#{host}"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for the given host in the future? + ["nixos-rebuild", operation, "--flake", f"{flake_archive['path']}#{host}"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for the given host in the future? check=False ) # fmt: skip -def remote(operation, host, reboot=False): # noqa: FBT002 - having reboot as a Boolean positional argument is fine I think # fmt: skip +def remote(operation, host, flake_archive, reboot=False): # noqa: FBT002 - having reboot as a Boolean positional argument is fine I think # fmt: skip config = None try: - with open("deployment_configuration.json") as config_file: + with open(f"{flake_archive['path']}/deployment_configuration.json") as config_file: try: config = json.load(config_file) except json.JSONDecodeError: @@ -62,7 +82,7 @@ def remote(operation, host, reboot=False): # noqa: FBT002 - having reboot as a pass if not target_hostname: nix_config_fqdn = subprocess.run( - ["nix", "eval", "--raw", f".#nixosConfigurations.{host}.config.networking.fqdn"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for host in the future? + ["nix", "eval", "--raw", f"{flake_archive['path']}#nixosConfigurations.{host}.config.networking.fqdn"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for host in the future? capture_output=True, text=True, check=False, @@ -119,7 +139,7 @@ def remote(operation, host, reboot=False): # noqa: FBT002 - having reboot as a msg.info(f"Running nixos-rebuild {operation} for {host} on {target_host}...") nixos_rebuild = subprocess.run( - ["nixos-rebuild", operation, "--flake", f".#{host}", "--target-host", target_host, "--use-substitutes", "--use-remote-sudo"], # noqa: S607, S603, E501 - can't know what the users system looks like, maybe do some kind of validation for the given host in the future?, not breaking up the command makes it more readable + ["nixos-rebuild", operation, "--flake", f"{flake_archive['path']}#{host}", "--target-host", target_host, "--use-substitutes", "--use-remote-sudo"], # noqa: S607, S603, E501 - can't know what the users system looks like, maybe do some kind of validation for the given host in the future?, not breaking up the command makes it more readable env=nixos_rebuild_env, check=False, ) # fmt: skip