Templates: migrate clan templates to flake identifiers

This commit is contained in:
Johannes Kirschbauer
2025-07-06 13:31:46 +02:00
parent e18fe13430
commit f2db543651
4 changed files with 100 additions and 75 deletions

View File

@@ -4,36 +4,17 @@ import logging
from pathlib import Path
from clan_lib.clan.create import CreateOptions, create_clan
from clan_lib.templates import (
InputPrio,
)
log = logging.getLogger(__name__)
def register_create_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--input",
type=str,
help="""Flake input name to use as template source
can be specified multiple times, inputs are tried in order of definition
Example: --input clan --input clan-core
""",
action="append",
default=[],
)
parser.add_argument(
"--no-self",
help="Do not look into own flake for templates",
action="store_true",
default=False,
)
parser.add_argument(
"--template",
type=str,
help="Clan template name",
help="""Reference to the template to use for the clan. default="default". In the format '<flake_ref>#template_name' Where <flake_ref> is a flake reference (e.g. github:org/repo) or a local path (e.g. '.' ).
Omitting '<flake_ref>#' will use the builtin templates (e.g. just 'default' from clan-core ).
""",
default="default",
)
@@ -59,19 +40,10 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
)
def create_flake_command(args: argparse.Namespace) -> None:
if len(args.input) == 0:
args.input = ["clan", "clan-core"]
if args.no_self:
input_prio = InputPrio.try_inputs(tuple(args.input))
else:
input_prio = InputPrio.try_self_then_inputs(tuple(args.input))
create_clan(
CreateOptions(
input_prio=input_prio,
dest=args.path,
template_name=args.template,
template=args.template,
setup_git=not args.no_git,
src_flake=args.flake,
update_clan=not args.no_update,

View File

@@ -4,16 +4,12 @@ from pathlib import Path
from clan_lib.api import API
from clan_lib.cmd import RunOpts, run
from clan_lib.dirs import clan_templates
from clan_lib.errors import ClanError
from clan_lib.flake import Flake
from clan_lib.nix import nix_command, nix_metadata, nix_shell
from clan_lib.persist.inventory_store import InventorySnapshot, InventoryStore
from clan_lib.templates import (
InputPrio,
TemplateName,
get_template,
)
from clan_lib.templates.filesystem import copy_from_nixstore
from clan_lib.templates.handler import clan_template
log = logging.getLogger(__name__)
@@ -21,9 +17,9 @@ log = logging.getLogger(__name__)
@dataclass
class CreateOptions:
dest: Path
template_name: str
template: str
src_flake: Flake | None = None
input_prio: InputPrio | None = None
setup_git: bool = True
initial: InventorySnapshot | None = None
update_clan: bool = True
@@ -47,44 +43,31 @@ def create_clan(opts: CreateOptions) -> None:
log.warning("Setting src_flake to None")
opts.src_flake = None
template = get_template(
TemplateName(opts.template_name),
"clan",
input_prio=opts.input_prio,
clan_dir=opts.src_flake,
)
log.info(f"Found template '{template.name}' in '{template.input_variant}'")
if opts.src_flake is None:
opts.src_flake = Flake(str(clan_templates()))
if dest.exists():
dest /= template.name
with clan_template(
opts.src_flake, template_ident=opts.template, dst_dir=opts.dest
) as _clan_dir:
if opts.setup_git:
run(git_command(dest, "init"))
run(git_command(dest, "add", "."))
if dest.exists():
msg = f"Destination directory {dest} already exists"
raise ClanError(msg)
# check if username is set
has_username = run(
git_command(dest, "config", "user.name"), RunOpts(check=False)
)
if has_username.returncode != 0:
run(git_command(dest, "config", "user.name", "clan-tool"))
src = Path(template.src["path"])
has_username = run(
git_command(dest, "config", "user.email"), RunOpts(check=False)
)
if has_username.returncode != 0:
run(git_command(dest, "config", "user.email", "clan@example.com"))
copy_from_nixstore(src, dest)
if opts.setup_git:
run(git_command(dest, "init"))
run(git_command(dest, "add", "."))
# check if username is set
has_username = run(
git_command(dest, "config", "user.name"), RunOpts(check=False)
)
if has_username.returncode != 0:
run(git_command(dest, "config", "user.name", "clan-tool"))
has_username = run(
git_command(dest, "config", "user.email"), RunOpts(check=False)
)
if has_username.returncode != 0:
run(git_command(dest, "config", "user.email", "clan@example.com"))
if opts.update_clan:
run(nix_command(["flake", "update"]), RunOpts(cwd=dest))
if opts.update_clan:
run(nix_command(["flake", "update"]), RunOpts(cwd=dest))
if opts.initial:
inventory_store = InventoryStore(flake=Flake(str(opts.dest)))

View File

@@ -99,3 +99,73 @@ def machine_template(
finally:
# If no error occurred, the machine directory is kept
pass
@contextmanager
def clan_template(flake: Flake, template_ident: str, dst_dir: Path) -> Iterator[Path]:
"""
Create a clan from a template.
This function will copy the template files to a new clan directory
:param flake: The flake to create the machine in.
:param template_ident: The identifier of the template to use. Example ".#template_name"
:param dst: The name of the directory to create.
Example usage:
>>> with clan_template(
... Flake("/home/johannes/git/clan-core"), ".#new-machine", "my-machine"
... ) as clan_dir:
... # Use `clan_dir` here if you want to access the created directory
... The directory is removed if the context raised any errors.
... Only if the context is exited without errors, it is kept.
"""
# Get the clan template from the specifier
[flake_ref, template_selector] = transform_url("clan", template_ident, flake=flake)
# For pretty error messages
printable_template_ref = f"{flake_ref}#{template_selector}"
template_flake = Flake(flake_ref)
try:
template = template_flake.select(template_selector)
except ClanError as e:
msg = f"Failed to select template '{template_ident}' from flake '{flake_ref}' (via attribute path: {printable_template_ref})"
raise ClanError(msg) from e
src = template.get("path")
if not src:
msg = f"Malformed template: {printable_template_ref} does not have a 'path' attribute"
raise ClanError(msg)
src_path = Path(src).resolve()
realize_nix_path(template_flake, str(src_path))
if not src_path.exists():
msg = f"Template {printable_template_ref} does not exist at {src_path}"
raise ClanError(msg)
if not src_path.is_dir():
msg = f"Template {printable_template_ref} is not a directory at {src_path}"
raise ClanError(msg)
if dst_dir.exists():
msg = f"Destination directory {dst_dir} already exists"
raise ClanError(msg)
copy_from_nixstore(src_path, dst_dir)
try:
yield dst_dir
except Exception as e:
log.error(f"An error occurred inside the 'clan_template' context: {e}")
log.info(f"Removing left-over directory: {dst_dir}")
shutil.rmtree(dst_dir, ignore_errors=True)
raise
finally:
# If no error occurred, the directory is kept
pass

View File

@@ -137,7 +137,7 @@ def test_clan_create_api(
# TODO: We need to generate a lock file for the templates
clan_cli.clan.create.create_clan(
clan_cli.clan.create.CreateOptions(
template_name="minimal", dest=dest_clan_dir, update_clan=False
template="minimal", dest=dest_clan_dir, update_clan=False
)
)
assert dest_clan_dir.is_dir()