diff --git a/pkgs/clan-cli/clan_lib/templates/handler.py b/pkgs/clan-cli/clan_lib/templates/handler.py index 9fecef37b..cecc887c3 100644 --- a/pkgs/clan-cli/clan_lib/templates/handler.py +++ b/pkgs/clan-cli/clan_lib/templates/handler.py @@ -46,7 +46,7 @@ def with_machine_template( ) # Get the clan template from the specifier - [flake_ref, template_selector] = transform_url("machine", template_ident) + [flake_ref, template_selector] = transform_url("machine", template_ident, local_path=flake) template_flake = Flake(flake_ref) template = template_flake.select(template_selector) diff --git a/pkgs/clan-cli/clan_lib/templates/template_url.py b/pkgs/clan-cli/clan_lib/templates/template_url.py index 2e85394b7..0f4586f27 100644 --- a/pkgs/clan-cli/clan_lib/templates/template_url.py +++ b/pkgs/clan-cli/clan_lib/templates/template_url.py @@ -1,40 +1,58 @@ import logging +from pathlib import Path +from typing import Protocol from clan_lib.errors import ClanError log = logging.getLogger(__name__) -def transform_url(template_type: str, identifier: str) -> tuple[str, str]: +class Flake(Protocol): + """ + Protocol for a local flake, which has a path attribute. + Pass clan_lib.flake.Flake or any other object that implements this protocol. + """ + + @property + def path(self) -> Path: ... + + +def transform_url( + template_type: str, identifier: str, local_path: Flake +) -> tuple[str, str]: """ Transform a template flake ref by injecting the context (clan|machine|disko) into the url. We do this for shorthand notation of URLs. - If the attribute selector path is longer than one, don't transform it. + If the attribute selector path is explicitly selecting an attribute, we don't transform it. + + :param template_type: The type of the template (clan, machine, disko) + :param identifier: The identifier of the template, which can be a flake reference with a fragment. + :param local_path: The local flake path, which is used to resolve to a local flake reference, i.e. ".#" shorthand. Examples: - # injects "machine" as context + 1. injects "machine" as context clan machines create --template .#new-machine or clan machines create --template #new-machine -> .#clan.templates.machine.new-machine - # injects "clan" as context + 2. injects "clan" as context clan create --template .#default -> .#clan.templates.clan.default - # Dont transform explicit paths (e.g. when more than one attribute selector is present) + 3. Dont transform explicit paths (e.g. when more than one attribute selector is present) clan machines create --template .#clan.templates.machine.new-machine -> .#clan.templates.machine.new-machine clan machines create --template .#"new.machine" -> .#clan.templates.machine."new.machine" - # Builtin templates + 4. Builtin templates clan machines create --template new.machine -> clanInternals.templates.machine."new.machine" - # Remote templates + 5. Remote templates clan machines create --template github:/org/repo#new.machine -> clanInternals.templates.machine."new.machine" @@ -55,10 +73,14 @@ def transform_url(template_type: str, identifier: str) -> tuple[str, str]: [flake_ref, selector] = ( identifier.split("#", 1) if "#" in identifier else ["", identifier] ) + # Substitute the flake reference with the local flake path if it is empty or just a dot. + # This is required if the command will be executed from a different place, than the local flake root. + if not flake_ref or flake_ref == ".": + flake_ref = str(local_path.path) if "#" not in identifier: # No fragment, so we assume its a builtin template - return ("", f'clanInternals.templates.{template_type}."{selector}"') + return (flake_ref, f'clanInternals.templates.{template_type}."{selector}"') # TODO: implement support for quotes in the tail "a.b".c # If the tail contains a dot, or is quoted we assume its a path and don't transform it. diff --git a/pkgs/clan-cli/clan_lib/templates/template_url_test.py b/pkgs/clan-cli/clan_lib/templates/template_url_test.py index ecef1c88d..9ec74805b 100644 --- a/pkgs/clan-cli/clan_lib/templates/template_url_test.py +++ b/pkgs/clan-cli/clan_lib/templates/template_url_test.py @@ -20,8 +20,10 @@ def test_transform_url_self_explizit_dot() -> None: user_input = ".#new-machine" expected_selector = 'clan.templates.machine."new-machine"' - flake_ref, selector = transform_url(template_type, user_input) - assert flake_ref == "." + flake_ref, selector = transform_url( + template_type, user_input, local_path=local_path + ) + assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -29,8 +31,10 @@ def test_transform_url_self_no_dot() -> None: user_input = "#new-machine" expected_selector = 'clan.templates.machine."new-machine"' - flake_ref, selector = transform_url(template_type, user_input) - assert flake_ref == "" + flake_ref, selector = transform_url( + template_type, user_input, local_path=local_path + ) + assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -38,8 +42,10 @@ def test_transform_url_builtin_template() -> None: user_input = "new-machine" expected_selector = 'clanInternals.templates.machine."new-machine"' - flake_ref, selector = transform_url(template_type, user_input) - assert flake_ref == "" + flake_ref, selector = transform_url( + template_type, user_input, local_path=local_path + ) + assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -47,7 +53,9 @@ def test_transform_url_remote_template() -> None: user_input = "github:/org/repo#new-machine" expected_selector = 'clan.templates.machine."new-machine"' - flake_ref, selector = transform_url(template_type, user_input) + flake_ref, selector = transform_url( + template_type, user_input, local_path=local_path + ) assert flake_ref == "github:/org/repo" assert selector == expected_selector @@ -57,8 +65,10 @@ def test_transform_url_explicit_path() -> None: user_input = ".#clan.templates.machine.new-machine" expected_selector = "clan.templates.machine.new-machine" - flake_ref, selector = transform_url(template_type, user_input) - assert flake_ref == "." + flake_ref, selector = transform_url( + template_type, user_input, local_path=local_path + ) + assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -66,16 +76,20 @@ def test_transform_url_explicit_path() -> None: def test_transform_url_quoted_selector() -> None: user_input = '.#"new.machine"' expected_selector = '"new.machine"' - flake_ref, selector = transform_url(template_type, user_input) - assert flake_ref == "." + flake_ref, selector = transform_url( + template_type, user_input, local_path=local_path + ) + assert flake_ref == str(local_path.path) assert selector == expected_selector def test_single_quote_selector() -> None: user_input = ".#'new.machine'" expected_selector = "'new.machine'" - flake_ref, selector = transform_url(template_type, user_input) - assert flake_ref == "." + flake_ref, selector = transform_url( + template_type, user_input, local_path=local_path + ) + assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -83,7 +97,9 @@ def test_custom_template_path() -> None: user_input = "github:/org/repo#my.templates.custom.machine" expected_selector = "my.templates.custom.machine" - flake_ref, selector = transform_url(template_type, user_input) + flake_ref, selector = transform_url( + template_type, user_input, local_path=local_path + ) assert flake_ref == "github:/org/repo" assert selector == expected_selector @@ -93,7 +109,9 @@ def test_full_url_query_and_fragment() -> None: expected_flake_ref = "github:/org/repo?query=param" expected_selector = "my.templates.custom.machine" - flake_ref, selector = transform_url(template_type, user_input) + flake_ref, selector = transform_url( + template_type, user_input, local_path=local_path + ) assert flake_ref == expected_flake_ref assert selector == expected_selector @@ -102,15 +120,17 @@ def test_custom_template_type() -> None: user_input = "#my.templates.custom.machine" expected_selector = "my.templates.custom.machine" - flake_ref, selector = transform_url("custom", user_input) - assert flake_ref == "" + flake_ref, selector = transform_url("custom", user_input, local_path=local_path) + assert flake_ref == str(local_path.path) assert selector == expected_selector def test_malformed_identifier() -> None: user_input = "github:/org/repo#my.templates.custom.machine#extra" with pytest.raises(ClanError) as exc_info: - _flake_ref, _selector = transform_url(template_type, user_input) + _flake_ref, _selector = transform_url( + template_type, user_input, local_path=local_path + ) assert isinstance(exc_info.value, ClanError) assert (