Merge pull request 'cli-prep' (#40) from cli-prep into main

This commit is contained in:
clan-bot
2023-07-28 08:49:23 +00:00
12 changed files with 95 additions and 52 deletions

View File

@@ -0,0 +1,25 @@
import os
import sys
from pathlib import Path
def get_clan_flake_toplevel() -> Path:
"""Returns the path to the toplevel of the clan flake"""
initial_path = Path(os.getcwd())
path = Path(initial_path)
while path.parent == path:
project_files = [".clan-flake"]
for project_file in project_files:
if (path / project_file).exists():
return path
path = path.parent
return initial_path
def user_data_dir() -> Path:
if sys.platform == "win32":
raise NotImplementedError("Windows is not supported")
elif sys.platform == "darwin":
return Path(os.path.expanduser("~/Library/Application Support/"))
else:
return Path(os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share")))

View File

@@ -0,0 +1,4 @@
class ClanError(Exception):
"""Base class for exceptions in this module."""
pass

View File

@@ -0,0 +1,25 @@
import sys
from typing import IO, Any, Callable
def is_interactive() -> bool:
"""Returns true if the current process is interactive"""
return sys.stdin.isatty() and sys.stdout.isatty()
def color_text(code: int, file: IO[Any] = sys.stdout) -> Callable[[str], None]:
"""
Print with color if stderr is a tty
"""
def wrapper(text: str) -> None:
if file.isatty():
print(f"\x1b[{code}m{text}\x1b[0m", file=file)
else:
print(text, file=file)
return wrapper
warn = color_text(91, file=sys.stderr)
info = color_text(92, file=sys.stderr)

View File

@@ -8,6 +8,7 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any, Iterator, Optional from typing import Any, Iterator, Optional
from ..errors import ClanError
from ..nix import nix_shell from ..nix import nix_shell
@@ -30,11 +31,11 @@ def try_connect_port(port: int) -> bool:
return result == 0 return result == 0
def find_free_port(port_range: range) -> int: def find_free_port(port_range: range) -> Optional[int]:
for port in port_range: for port in port_range:
if try_bind_port(port): if try_bind_port(port):
return port return port
raise Exception("cannot find a free port") return None
class ZerotierController: class ZerotierController:
@@ -86,6 +87,8 @@ class ZerotierController:
def zerotier_controller() -> Iterator[ZerotierController]: def zerotier_controller() -> Iterator[ZerotierController]:
# This check could be racy but it's unlikely in practice # This check could be racy but it's unlikely in practice
controller_port = find_free_port(range(10000, 65535)) controller_port = find_free_port(range(10000, 65535))
if controller_port is None:
raise ClanError("cannot find a free port for zerotier controller")
cmd = nix_shell(["bash", "zerotierone"], ["bash", "-c", "command -v zerotier-one"]) cmd = nix_shell(["bash", "zerotierone"], ["bash", "-c", "command -v zerotier-one"])
res = subprocess.run( res = subprocess.run(
cmd, cmd,
@@ -95,10 +98,10 @@ def zerotier_controller() -> Iterator[ZerotierController]:
) )
zerotier_exe = res.stdout.strip() zerotier_exe = res.stdout.strip()
if zerotier_exe is None: if zerotier_exe is None:
raise Exception("cannot find zerotier-one executable") raise ClanError("cannot find zerotier-one executable")
if not zerotier_exe.startswith("/nix/store"): if not zerotier_exe.startswith("/nix/store"):
raise Exception( raise ClanError(
f"zerotier-one executable needs to come from /nix/store: {zerotier_exe}" f"zerotier-one executable needs to come from /nix/store: {zerotier_exe}"
) )
@@ -136,7 +139,7 @@ def zerotier_controller() -> Iterator[ZerotierController]:
while not try_connect_port(controller_port): while not try_connect_port(controller_port):
status = p.poll() status = p.poll()
if status is not None: if status is not None:
raise Exception( raise ClanError(
f"zerotier-one has been terminated unexpected with {status}" f"zerotier-one has been terminated unexpected with {status}"
) )
time.sleep(0.1) time.sleep(0.1)

View File

View File

@@ -0,0 +1,14 @@
import os
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def mock_env(**environ: str) -> Iterator[None]:
original_environ = dict(os.environ)
os.environ.update(environ)
try:
yield
finally:
os.environ.clear()
os.environ.update(original_environ)

View File

@@ -1,7 +1,5 @@
import os
import sys import sys
from contextlib import contextmanager from typing import Union
from typing import Iterator, Union
import pytest import pytest
import pytest_subprocess.fake_process import pytest_subprocess.fake_process
@@ -9,6 +7,8 @@ from pytest_subprocess import utils
import clan_cli.ssh import clan_cli.ssh
from .environment import mock_env
def test_no_args( def test_no_args(
capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch
@@ -20,17 +20,6 @@ def test_no_args(
assert captured.err.startswith("usage:") assert captured.err.startswith("usage:")
@contextmanager
def mock_env(**environ: str) -> Iterator[None]:
original_environ = dict(os.environ)
os.environ.update(environ)
try:
yield
finally:
os.environ.clear()
os.environ.update(original_environ)
# using fp fixture from pytest-subprocess # using fp fixture from pytest-subprocess
def test_ssh_no_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None: def test_ssh_no_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None:
with mock_env(CLAN_FLAKE="/mocked-flake"): with mock_env(CLAN_FLAKE="/mocked-flake"):

View File

@@ -0,0 +1,2 @@
# DO NOT DELETE
# This file is used by the clan cli to discover a clan flake

View File

@@ -0,0 +1,9 @@
# AUTOMATICALLY GENERATED by clan
{ ... }: {
imports =
let
relPaths = builtins.fromJSON (builtins.readFile ./imports.json);
paths = map (path: ./. + path) relPaths;
in
paths;
}

View File

@@ -1,16 +0,0 @@
# This file provides backward compatibility to nix < 2.4 clients
let
flake =
import
(
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
)
{ src = ./.; };
in
flake.defaultNix

View File

@@ -1,25 +1,15 @@
{ {
description = ""; description = "<Put your description here>";
inputs = { inputs = {
clan-core.url = "git+https://git.clan.lol/clan/clan-core";
flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.url = "github:hercules-ci/flake-parts";
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
nixpkgs.url = "nixpkgs/nixos-unstable";
}; };
outputs = inputs @ { flake-parts, ... }: outputs = inputs @ { flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } { flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
systems = builtins.fromJSON (builtins.readFile ./systems.json); ./clan-flake-module.nix
];
imports =
let
relPaths = builtins.fromJSON (builtins.readFile ./imports.json);
paths = map (path: ./. + path) relPaths;
in
paths;
}; };
} }

View File

@@ -1,2 +0,0 @@
(import ./default.nix).devShells.${builtins.currentSystem}.default
or (throw "dev-shell not defined. Cannot find flake attribute devShell.${builtins.currentSystem}.default")