API: Added /api/flake/create. Fixed vscode search settings. Moved clan create to clan flake create
All checks were successful
checks-impure / test (pull_request) Successful in 57s
checks / test (pull_request) Successful in 2m3s

This commit is contained in:
Luis Hebendanz 2023-10-09 14:01:34 +02:00
parent 9c74c4d661
commit b49433958b
14 changed files with 157 additions and 52 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.direnv .direnv
**/testdir
democlan democlan
result* result*
/pkgs/clan-cli/clan_cli/nixpkgs /pkgs/clan-cli/clan_cli/nixpkgs

View File

@ -12,4 +12,7 @@
], ],
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"search.exclude": {
"**/.direnv": true
},
} }

View File

@ -3,7 +3,7 @@ import sys
from types import ModuleType from types import ModuleType
from typing import Optional from typing import Optional
from . import config, create, join, machines, secrets, vms, webui from . import config, flake, join, machines, secrets, vms, webui
from .ssh import cli as ssh_cli from .ssh import cli as ssh_cli
argcomplete: Optional[ModuleType] = None argcomplete: Optional[ModuleType] = None
@ -24,10 +24,10 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser:
subparsers = parser.add_subparsers() subparsers = parser.add_subparsers()
parser_create = subparsers.add_parser( parser_flake = subparsers.add_parser(
"create", help="create a clan flake inside the current directory" "flake", help="create a clan flake inside the current directory"
) )
create.register_parser(parser_create) flake.register_parser(parser_flake)
parser_join = subparsers.add_parser("join", help="join a remote clan") parser_join = subparsers.add_parser("join", help="join a remote clan")
join.register_parser(parser_join) join.register_parser(parser_join)

View File

@ -1,18 +1,29 @@
import asyncio import asyncio
import logging import logging
import shlex import shlex
from pathlib import Path
from typing import Optional, Tuple
from .errors import ClanError from .errors import ClanError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
async def run(cmd: list[str]) -> bytes: async def run(cmd: list[str], cwd: Optional[Path] = None) -> Tuple[bytes, bytes]:
log.debug(f"$: {shlex.join(cmd)}") log.debug(f"$: {shlex.join(cmd)}")
cwd_res = None
if cwd is not None:
if not cwd.exists():
raise ClanError(f"Working directory {cwd} does not exist")
if not cwd.is_dir():
raise ClanError(f"Working directory {cwd} is not a directory")
cwd_res = cwd.resolve()
log.debug(f"Working directory: {cwd_res}")
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
*cmd, *cmd,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
cwd=cwd_res,
) )
stdout, stderr = await proc.communicate() stdout, stderr = await proc.communicate()
@ -25,4 +36,4 @@ command output:
{stderr.decode("utf-8")} {stderr.decode("utf-8")}
""" """
) )
return stdout return stdout, stderr

View File

@ -1,25 +0,0 @@
# !/usr/bin/env python3
import argparse
import subprocess
from .nix import nix_command
def create(args: argparse.Namespace) -> None:
# TODO create clan template in flake
subprocess.run(
nix_command(
[
"flake",
"init",
"-t",
"git+https://git.clan.lol/clan/clan-core#new-clan",
]
),
check=True,
)
# takes a (sub)parser and configures it
def register_parser(parser: argparse.ArgumentParser) -> None:
parser.set_defaults(func=create)

View File

@ -0,0 +1,16 @@
# !/usr/bin/env python3
import argparse
from .create import register_create_parser
# takes a (sub)parser and configures it
def register_parser(parser: argparse.ArgumentParser) -> None:
subparser = parser.add_subparsers(
title="command",
description="the command to run",
help="the command to run",
required=True,
)
update_parser = subparser.add_parser("create", help="Create a clan flake")
register_create_parser(update_parser)

View File

@ -0,0 +1,47 @@
# !/usr/bin/env python3
import argparse
import asyncio
from pathlib import Path
from typing import Tuple
from ..async_cmd import run
from ..errors import ClanError
from ..nix import nix_command
DEFAULT_URL = "git+https://git.clan.lol/clan/clan-core#new-clan"
async def create_flake(directory: Path, url: str) -> Tuple[bytes, bytes]:
if not directory.exists():
directory.mkdir()
flake_command = nix_command(
[
"flake",
"init",
"-t",
url,
]
)
stdout, stderr = await run(flake_command, directory)
return stdout, stderr
def create_flake_command(args: argparse.Namespace) -> None:
try:
stdout, stderr = asyncio.run(create_flake(args.directory, DEFAULT_URL))
print(stderr.decode("utf-8"), end="")
print(stdout.decode("utf-8"), end="")
except ClanError as e:
print(e)
exit(1)
# takes a (sub)parser and configures it
def register_create_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"directory",
type=Path,
help="output directory for the flake",
)
# parser.add_argument("name", type=str, help="name of the flake")
parser.set_defaults(func=create_flake_command)

View File

@ -8,6 +8,7 @@ import sys
import threading import threading
import traceback import traceback
from enum import Enum from enum import Enum
from pathlib import Path
from typing import Any, Iterator, Optional, Type, TypeVar from typing import Any, Iterator, Optional, Type, TypeVar
from uuid import UUID, uuid4 from uuid import UUID, uuid4
@ -30,14 +31,30 @@ class Command:
self._output.put(None) self._output.put(None)
self.done = True self.done = True
def run(self, cmd: list[str], env: Optional[dict[str, str]] = None) -> None: def run(
self,
cmd: list[str],
env: Optional[dict[str, str]] = None,
cwd: Optional[Path] = None,
) -> None:
self.running = True self.running = True
self.log.debug(f"Running command: {shlex.join(cmd)}") self.log.debug(f"Running command: {shlex.join(cmd)}")
cwd_res = None
if cwd is not None:
if not cwd.exists():
raise ClanError(f"Working directory {cwd} does not exist")
if not cwd.is_dir():
raise ClanError(f"Working directory {cwd} is not a directory")
cwd_res = cwd.resolve()
self.log.debug(f"Working directory: {cwd_res}")
self.p = subprocess.Popen( self.p = subprocess.Popen(
cmd, cmd,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
encoding="utf-8", encoding="utf-8",
cwd=cwd_res,
env=env, env=env,
) )
assert self.p.stdout is not None and self.p.stderr is not None assert self.p.stdout is not None and self.p.stderr is not None
@ -106,7 +123,7 @@ class BaseTask:
def run(self) -> None: def run(self) -> None:
raise NotImplementedError raise NotImplementedError
## TODO: If two clients are connected to the same task, ## TODO: Test when two clients are connected to the same task
def log_lines(self) -> Iterator[str]: def log_lines(self) -> Iterator[str]:
with self.logs_lock: with self.logs_lock:
for proc in self.procs: for proc in self.procs:

View File

@ -26,7 +26,7 @@ async def inspect_vm(flake_url: str, flake_attr: str) -> VmConfig:
f'{flake_url}#clanInternals.machines."{system}"."{flake_attr}".config.system.clan.vm.config' f'{flake_url}#clanInternals.machines."{system}"."{flake_attr}".config.system.clan.vm.config'
] ]
) )
stdout = await run(cmd) stdout, stderr = await run(cmd)
data = json.loads(stdout) data = json.loads(stdout)
return VmConfig(flake_url=flake_url, flake_attr=flake_attr, **data) return VmConfig(flake_url=flake_url, flake_attr=flake_attr, **data)

View File

@ -1,12 +1,18 @@
import json import json
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from pathlib import Path from pathlib import Path
from typing import Annotated
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, Body, HTTPException, Response, status
from clan_cli.webui.schemas import FlakeAction, FlakeAttrResponse, FlakeResponse from clan_cli.webui.schemas import (
FlakeAction,
FlakeAttrResponse,
FlakeResponse,
)
from ...async_cmd import run from ...async_cmd import run
from ...flake import create
from ...nix import nix_command, nix_flake_show from ...nix import nix_command, nix_flake_show
router = APIRouter() router = APIRouter()
@ -14,7 +20,7 @@ router = APIRouter()
async def get_attrs(url: str) -> list[str]: async def get_attrs(url: str) -> list[str]:
cmd = nix_flake_show(url) cmd = nix_flake_show(url)
stdout = await run(cmd) stdout, stderr = await run(cmd)
data: dict[str, dict] = {} data: dict[str, dict] = {}
try: try:
@ -45,7 +51,7 @@ async def inspect_flake(
# Extract the flake from the given URL # Extract the flake from the given URL
# We do this by running 'nix flake prefetch {url} --json' # We do this by running 'nix flake prefetch {url} --json'
cmd = nix_command(["flake", "prefetch", url, "--json", "--refresh"]) cmd = nix_command(["flake", "prefetch", url, "--json", "--refresh"])
stdout = await run(cmd) stdout, stderr = await run(cmd)
data: dict[str, str] = json.loads(stdout) data: dict[str, str] = json.loads(stdout)
if data.get("storePath") is None: if data.get("storePath") is None:
@ -60,3 +66,15 @@ async def inspect_flake(
actions.append(FlakeAction(id="vms/create", uri="api/vms/create")) actions.append(FlakeAction(id="vms/create", uri="api/vms/create"))
return FlakeResponse(content=content, actions=actions) return FlakeResponse(content=content, actions=actions)
@router.post("/api/flake/create")
async def create_flake(
destination: Annotated[Path, Body()], url: Annotated[str, Body()]
) -> Response:
stdout, stderr = await create.create_flake(destination, url)
print(stderr.decode("utf-8"), end="")
print(stdout.decode("utf-8"), end="")
resp = Response()
resp.status_code = status.HTTP_201_CREATED
return resp

View File

@ -60,6 +60,10 @@ class FlakeAction(BaseModel):
uri: str uri: str
class FlakeCreateResponse(BaseModel):
uuid: str
class FlakeResponse(BaseModel): class FlakeResponse(BaseModel):
content: str content: str
actions: List[FlakeAction] actions: List[FlakeAction]

View File

@ -31,6 +31,8 @@ def open_browser(base_url: str, sub_url: str) -> None:
def _open_browser(url: str) -> subprocess.Popen: def _open_browser(url: str) -> subprocess.Popen:
for browser in ("firefox", "iceweasel", "iceape", "seamonkey"): for browser in ("firefox", "iceweasel", "iceape", "seamonkey"):
if shutil.which(browser): if shutil.which(browser):
# Do not add a new profile, as it will break in combination with
# the -kiosk flag.
cmd = [ cmd = [
browser, browser,
"-kiosk", "-kiosk",

View File

@ -1,12 +0,0 @@
from pathlib import Path
import pytest
from cli import Cli
@pytest.mark.impure
def test_template(monkeypatch: pytest.MonkeyPatch, temporary_dir: Path) -> None:
monkeypatch.chdir(temporary_dir)
cli = Cli()
cli.run(["create"])
assert (temporary_dir / ".clan-flake").exists()

View File

@ -3,6 +3,7 @@ import subprocess
from pathlib import Path from pathlib import Path
import pytest import pytest
from api import TestClient
from cli import Cli from cli import Cli
@ -11,6 +12,25 @@ def cli() -> Cli:
return Cli() return Cli()
@pytest.mark.impure
def test_create_flake_api(
monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_dir: Path
) -> None:
flake_dir = temporary_dir / "flake_dir"
flake_dir_str = str(flake_dir.resolve())
response = api.post(
"/api/flake/create",
json=dict(
destination=flake_dir_str,
url="git+https://git.clan.lol/clan/clan-core#new-clan",
),
)
assert response.status_code == 201, "Failed to create flake"
assert (flake_dir / ".clan-flake").exists()
assert (flake_dir / "flake.nix").exists()
@pytest.mark.impure @pytest.mark.impure
def test_create_flake( def test_create_flake(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
@ -19,8 +39,11 @@ def test_create_flake(
cli: Cli, cli: Cli,
) -> None: ) -> None:
monkeypatch.chdir(temporary_dir) monkeypatch.chdir(temporary_dir)
cli.run(["create"]) flake_dir = temporary_dir / "flake_dir"
assert (temporary_dir / ".clan-flake").exists() flake_dir_str = str(flake_dir.resolve())
cli.run(["flake", "create", flake_dir_str])
assert (flake_dir / ".clan-flake").exists()
monkeypatch.chdir(flake_dir)
cli.run(["machines", "create", "machine1"]) cli.run(["machines", "create", "machine1"])
capsys.readouterr() # flush cache capsys.readouterr() # flush cache
cli.run(["machines", "list"]) cli.run(["machines", "list"])