Feat(template_url): substitute local refs

To execute the CLI in foreign directories
.#new-machine needs to get tranformed into /path/to/clan#new-machine
Otherwise it might pick-up some random flake that is in scope where the cli started executing
This commit is contained in:
Johannes Kirschbauer
2025-06-11 15:56:45 +02:00
parent 0280d3a6a6
commit 1249a18901
3 changed files with 69 additions and 27 deletions

View File

@@ -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)

View File

@@ -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.

View File

@@ -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 (