diff --git a/pkgs/clan-cli/clan_lib/templates/handler.py b/pkgs/clan-cli/clan_lib/templates/handler.py index c8189e2ef..deee2b39f 100644 --- a/pkgs/clan-cli/clan_lib/templates/handler.py +++ b/pkgs/clan-cli/clan_lib/templates/handler.py @@ -47,7 +47,7 @@ def machine_template( # Get the clan template from the specifier [flake_ref, template_selector] = transform_url( - "machine", template_ident, local_path=flake + "machine", template_ident, flake=flake ) template_flake = Flake(flake_ref) diff --git a/pkgs/clan-cli/clan_lib/templates/template_url.py b/pkgs/clan-cli/clan_lib/templates/template_url.py index 0f4586f27..d99e2f328 100644 --- a/pkgs/clan-cli/clan_lib/templates/template_url.py +++ b/pkgs/clan-cli/clan_lib/templates/template_url.py @@ -16,10 +16,10 @@ class Flake(Protocol): @property def path(self) -> Path: ... + def get_input_names(self) -> list[str]: ... -def transform_url( - template_type: str, identifier: str, local_path: Flake -) -> tuple[str, str]: + +def transform_url(template_type: str, identifier: str, flake: 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. @@ -56,6 +56,11 @@ def transform_url( clan machines create --template github:/org/repo#new.machine -> clanInternals.templates.machine."new.machine" + 6. Templates locked via inputs: + clan machines create --template clan-core#new-machine + path: clan-core matches one of the input attributes. + -> #inputs.clan-core.clan.templates.machine."new-machine" + As of URL specification (RFC 3986). scheme:[//[user:password@]host[:port]][/path][?query][#fragment] @@ -76,22 +81,28 @@ def transform_url( # 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) + flake_ref = str(flake.path) if "#" not in identifier: # No fragment, so we assume its a builtin template return (flake_ref, f'clanInternals.templates.{template_type}."{selector}"') + input_prefix = "" + if flake_ref in flake.get_input_names(): + # Interpret the flake reference as an input of the local flake. + input_prefix = f"inputs.{flake_ref}." + flake_ref = str(flake.path) + # 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. if '"' in selector or "'" in selector: log.warning( "Quotes in template paths are not yet supported. Please use unquoted paths." ) - return (flake_ref, selector) + return (flake_ref, input_prefix + selector) if "." in selector: - return (flake_ref, selector) + return (flake_ref, input_prefix + selector) # Tail doesn't contain a dot at this point, so we can inject the context. - return (flake_ref, f'clan.templates.{template_type}."{selector}"') + return (flake_ref, input_prefix + f'clan.templates.{template_type}."{selector}"') 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 9ec74805b..3193f0447 100644 --- a/pkgs/clan-cli/clan_lib/templates/template_url_test.py +++ b/pkgs/clan-cli/clan_lib/templates/template_url_test.py @@ -12,6 +12,9 @@ class DummyFlake: def __init__(self, path: str) -> None: self.path: Path = Path(path) + def get_input_names(self) -> list[str]: + return ["locked-input"] + local_path = DummyFlake(".") @@ -20,9 +23,7 @@ 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, local_path=local_path - ) + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -31,9 +32,7 @@ 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, local_path=local_path - ) + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -42,9 +41,7 @@ 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, local_path=local_path - ) + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -53,9 +50,7 @@ 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, local_path=local_path - ) + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) assert flake_ref == "github:/org/repo" assert selector == expected_selector @@ -65,9 +60,7 @@ 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, local_path=local_path - ) + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -76,9 +69,7 @@ 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, local_path=local_path - ) + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -86,9 +77,7 @@ def test_transform_url_quoted_selector() -> None: def test_single_quote_selector() -> None: user_input = ".#'new.machine'" expected_selector = "'new.machine'" - flake_ref, selector = transform_url( - template_type, user_input, local_path=local_path - ) + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -97,9 +86,7 @@ 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, local_path=local_path - ) + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) assert flake_ref == "github:/org/repo" assert selector == expected_selector @@ -109,9 +96,7 @@ 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, local_path=local_path - ) + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) assert flake_ref == expected_flake_ref assert selector == expected_selector @@ -120,7 +105,7 @@ 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, local_path=local_path) + flake_ref, selector = transform_url("custom", user_input, flake=local_path) assert flake_ref == str(local_path.path) assert selector == expected_selector @@ -129,7 +114,7 @@ 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, local_path=local_path + template_type, user_input, flake=local_path ) assert isinstance(exc_info.value, ClanError) @@ -137,3 +122,30 @@ def test_malformed_identifier() -> None: str(exc_info.value) == "Invalid template identifier: More than one '#' found. Please use a single '#'" ) + + +def test_locked_input_template() -> None: + user_input = "locked-input#new-machine" + expected_selector = 'inputs.locked-input.clan.templates.machine."new-machine"' + + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) + assert flake_ref == str(local_path.path) + assert selector == expected_selector + + +def test_locked_input_template_no_quotes() -> None: + user_input = 'locked-input#"new.machine"' + expected_selector = 'inputs.locked-input."new.machine"' + + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) + assert selector == expected_selector + assert flake_ref == str(local_path.path) + + +def test_locked_input_template_no_dot() -> None: + user_input = "locked-input#new.machine" + expected_selector = "inputs.locked-input.new.machine" + + flake_ref, selector = transform_url(template_type, user_input, flake=local_path) + assert selector == expected_selector + assert flake_ref == str(local_path.path)