diff --git a/dreadnode_cli/agent/cli.py b/dreadnode_cli/agent/cli.py index f1fb114..3c111ac 100644 --- a/dreadnode_cli/agent/cli.py +++ b/dreadnode_cli/agent/cli.py @@ -61,7 +61,7 @@ def init( print() project_name = Prompt.ask("Project name?", default=name or directory.name) - template = Template(Prompt.ask("Template?", choices=[t.value for t in Template], default=template)) + template = Template(Prompt.ask("Template?", choices=[t.value for t in Template], default=template.value)) directory.mkdir(exist_ok=True) diff --git a/dreadnode_cli/agent/docker.py b/dreadnode_cli/agent/docker.py index e6e5cc8..9113041 100644 --- a/dreadnode_cli/agent/docker.py +++ b/dreadnode_cli/agent/docker.py @@ -8,7 +8,12 @@ from rich.text import Text from dreadnode_cli.config import ServerConfig -from dreadnode_cli.defaults import DOCKER_REGISTRY_SUBDOMAIN, PLATFORM_BASE_DOMAIN +from dreadnode_cli.defaults import ( + DOCKER_REGISTRY_IMAGE_TAG, + DOCKER_REGISTRY_LOCAL_PORT, + DOCKER_REGISTRY_SUBDOMAIN, + PLATFORM_BASE_DOMAIN, +) try: client = docker.from_env() @@ -16,15 +21,31 @@ client = None +def get_local_registry_port() -> int: + if client is None: + raise Exception("Docker not available") + + for container in client.containers.list(): + if DOCKER_REGISTRY_IMAGE_TAG in container.image.tags: + ports = container.attrs["NetworkSettings"]["Ports"] + assert len(ports) == 1 + for _container_port, port_bindings in ports.items(): + if port_bindings: + for binding in port_bindings: + return int(binding["HostPort"]) + + # fallback to the default port if we can't find the running container + return DOCKER_REGISTRY_LOCAL_PORT + + def get_registry(config: ServerConfig) -> str: # fail early if docker is not available if client is None: raise Exception("Docker not available") # localhost is a special case - # TODO: Can we get this port dynamically? if "localhost" in config.url or "127.0.0.1" in config.url: - return "localhost:5005" + return f"localhost:{get_local_registry_port()}" prefix = "" if "staging-" in config.url: diff --git a/dreadnode_cli/agent/tests/test_docker.py b/dreadnode_cli/agent/tests/test_docker.py index da62fb5..37f78c2 100644 --- a/dreadnode_cli/agent/tests/test_docker.py +++ b/dreadnode_cli/agent/tests/test_docker.py @@ -5,13 +5,23 @@ import dreadnode_cli.agent.docker as docker from dreadnode_cli.config import ServerConfig +from dreadnode_cli.defaults import DOCKER_REGISTRY_IMAGE_TAG class MockImage: + def __init__(self, tags: list[str] | None = None) -> None: + self.tags = tags or [] + def tag(self, *args: t.Any, **kwargs: t.Any) -> None: pass +class MockContainer: + def __init__(self, image_tags: list[str], attrs: dict[str, t.Any]) -> None: + self.image = MockImage(image_tags) + self.attrs = attrs + + class MockDockerClient: """Simple mock Docker client for testing.""" @@ -37,6 +47,13 @@ class images: def get(id: str) -> MockImage: return MockImage() + class containers: + containers: list[MockContainer] = [] + + @staticmethod + def list(*args: t.Any, **kwargs: t.Any) -> list[MockContainer]: + return MockDockerClient.containers.containers + def _create_test_server_config(url: str = "https://crucible.dreadnode.io") -> ServerConfig: return ServerConfig( @@ -116,6 +133,20 @@ def test_get_registry() -> None: assert docker.get_registry(config) == "localhost:5005" +def test_get_local_registry_port_with_running_registry_container() -> None: + with pytest.MonkeyPatch.context() as mp: + mp.setattr( + docker.client.containers, + "containers", + [ + MockContainer( + [DOCKER_REGISTRY_IMAGE_TAG], {"NetworkSettings": {"Ports": {"5000/tcp": [{"HostPort": "12345"}]}}} + ) + ], + ) + assert docker.get_registry(_create_test_server_config("http://localhost:8000")) == "localhost:12345" + + def test_get_registry_without_schema() -> None: # Test without schema config = _create_test_server_config("crucible.dreadnode.io") diff --git a/dreadnode_cli/defaults.py b/dreadnode_cli/defaults.py index 95a388b..7ecb28a 100644 --- a/dreadnode_cli/defaults.py +++ b/dreadnode_cli/defaults.py @@ -14,6 +14,11 @@ PLATFORM_BASE_URL = os.getenv("DREADNODE_SERVER", f"https://crucible.{PLATFORM_BASE_DOMAIN}") # default docker registry subdomain DOCKER_REGISTRY_SUBDOMAIN = "registry" +# default docker registry local port +DOCKER_REGISTRY_LOCAL_PORT = 5005 +# default docker registry image tag +DOCKER_REGISTRY_IMAGE_TAG = "registry" + # path to the user configuration file USER_CONFIG_PATH = pathlib.Path( # allow overriding the user config file via env variable