Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions dreadnode/cli/platform/constants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import typing as t

PlatformService = t.Literal["api", "ui"]
API_SERVICE: PlatformService = "api"
UI_SERVICE: PlatformService = "ui"
PlatformService = t.Literal["dreadnode-api", "dreadnode-ui"]
API_SERVICE: PlatformService = "dreadnode-api"
UI_SERVICE: PlatformService = "dreadnode-ui"
SERVICES: list[PlatformService] = [API_SERVICE, UI_SERVICE]
VERSIONS_MANIFEST = "versions.json"

Expand Down
165 changes: 92 additions & 73 deletions dreadnode/cli/platform/docker_.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dataclasses import dataclass
from enum import Enum

import yaml
from pydantic import BaseModel, Field
from yaml import safe_dump

Expand Down Expand Up @@ -66,22 +67,33 @@ def __str__(self) -> str:
return result

def __eq__(self, other: object) -> bool:
"""Check if two DockerImage instances are equal."""
"""Check if two DockerImage instances are equal.

If they both have digests, compare digests.
If they both have tags, compare tags.

"""
if not isinstance(other, DockerImage):
return False
return (
self.repository == other.repository
and self.tag == other.tag
and self.digest == other.digest
)
if self.repository != other.repository:
return False
if self.digest and other.digest:
return self.digest == other.digest
if self.tag and other.tag:
return self.tag == other.tag
return False

def __ne__(self, other: object) -> bool:
"""Check if two DockerImage instances are not equal."""
return not self.__eq__(other)

def __hash__(self) -> int:
"""Make DockerImage hashable so it can be used in sets/dicts."""
return hash((self.repository, self.tag, self.digest))
"""Generate a hash for the DockerImage instance."""
if self.tag:
return hash((self.repository, self.tag))
if self.digest:
return hash((self.repository, self.digest))
return hash((self.repository,))


class DockerPSResult(BaseModel):
Expand All @@ -95,9 +107,77 @@ def is_running(self) -> bool:
return self.state == "running"


def _build_docker_compose_base_command(
selected_version: LocalVersionSchema,
) -> list[str]:
cmds = []
compose_files = [selected_version.compose_file]
env_files = [
selected_version.api_env_file,
selected_version.ui_env_file,
]

if (
selected_version.configure_overrides_compose_file.exists()
and selected_version.configure_overrides_env_file.exists()
):
compose_files.append(selected_version.configure_overrides_compose_file)
env_files.append(selected_version.configure_overrides_env_file)

for compose_file in compose_files:
cmds.extend(["-f", compose_file.as_posix()])

if selected_version.arg_overrides_env_file.exists():
env_files.append(selected_version.arg_overrides_env_file)

for env_file in env_files:
cmds.extend(["--env-file", env_file.as_posix()])
return cmds


def _check_docker_installed() -> bool:
"""Check if Docker is installed on the system."""
try:
cmd = ["docker", "--version"]
subprocess.run( # noqa: S603
cmd,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)

except subprocess.CalledProcessError:
print_error("Docker is not installed. Please install Docker and try again.")
return False

return True


def _check_docker_compose_installed() -> bool:
"""Check if Docker Compose is installed on the system."""
try:
cmd = ["docker", "compose", "--version"]
subprocess.run( # noqa: S603
cmd,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except subprocess.CalledProcessError:
print_error("Docker Compose is not installed. Please install Docker Compose and try again.")
return False
return True


def get_required_service_names(selected_version: LocalVersionSchema) -> list[str]:
"""Get the list of require service names from the docker-compose file."""
contents: dict[str, t.Any] = yaml.safe_load(selected_version.compose_file.read_text())
services = contents.get("services", {}) or {}
return [name for name, cfg in services.items() if isinstance(cfg, dict) and "x-required" in cfg]


def _run_docker_compose_command(
args: list[str],
# compose_file: Path,
timeout: int = 300,
stdin_input: str | None = None,
capture_output: CaptureOutput | None = None,
Expand Down Expand Up @@ -157,65 +237,6 @@ def _run_docker_compose_command(
return result


def _check_docker_installed() -> bool:
"""Check if Docker is installed on the system."""
try:
cmd = ["docker", "--version"]
subprocess.run( # noqa: S603
cmd,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)

except subprocess.CalledProcessError:
print_error("Docker is not installed. Please install Docker and try again.")
return False

return True


def _check_docker_compose_installed() -> bool:
"""Check if Docker Compose is installed on the system."""
try:
cmd = ["docker", "compose", "--version"]
subprocess.run( # noqa: S603
cmd,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except subprocess.CalledProcessError:
print_error("Docker Compose is not installed. Please install Docker Compose and try again.")
return False
return True


def _build_docker_compose_base_command(
selected_version: LocalVersionSchema,
) -> list[str]:
cmds = []
compose_files = [selected_version.compose_file]
env_files = [selected_version.api_env_file, selected_version.ui_env_file]

if (
selected_version.configure_overrides_compose_file.exists()
and selected_version.configure_overrides_env_file.exists()
):
compose_files.append(selected_version.configure_overrides_compose_file)
env_files.append(selected_version.configure_overrides_env_file)

for compose_file in compose_files:
cmds.extend(["-f", compose_file.as_posix()])

if selected_version.arg_overrides_env_file.exists():
env_files.append(selected_version.arg_overrides_env_file)

for env_file in env_files:
cmds.extend(["--env-file", env_file.as_posix()])
return cmds


def build_docker_compose_override_file(
services: list[PlatformService],
selected_version: LocalVersionSchema,
Expand All @@ -225,9 +246,7 @@ def build_docker_compose_override_file(
# and has an `env_file` attribute for the service
override = {
"services": {
f"platform-{service}": {
"env_file": [selected_version.configure_overrides_env_file.as_posix()]
}
f"{service}": {"env_file": [selected_version.configure_overrides_env_file.as_posix()]}
for service in services
},
}
Expand All @@ -242,15 +261,15 @@ def get_available_local_images() -> list[DockerImage]:
Returns:
list[str]: List of available Docker image names.
"""
cmd = ["docker", "images", "--format", ""]
cmd = ["docker", "images", "--format", "{{.Repository}}:{{.Tag}}@{{.Digest}}"]
cp = subprocess.run( # noqa: S603
cmd,
check=True,
text=True,
capture_output=True,
)
images: list[DockerImage] = []
for line in cp.stdout.splitlines():
for line in cp.stdout.splitlines()[1:]: # Skip header line
if line.strip():
img = DockerImage.from_string(line.strip())
images.append(img)
Expand Down
20 changes: 6 additions & 14 deletions dreadnode/cli/platform/status.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dreadnode.cli.platform.docker_ import docker_ps
from dreadnode.cli.platform.docker_ import docker_ps, get_required_service_names
from dreadnode.cli.platform.schemas import LocalVersionSchema
from dreadnode.cli.platform.utils.printing import print_error, print_success
from dreadnode.cli.platform.utils.versions import get_current_version, get_local_version
Expand All @@ -11,22 +11,14 @@ def platform_is_running(selected_version: LocalVersionSchema) -> bool:
tag: Optional image tag to use. If not provided, uses the current
version or downloads the latest available version.
"""

required_services = get_required_service_names(selected_version)
container_details = docker_ps(selected_version)
if not container_details:
return False
return all(
container.is_running
for container in container_details
if container.name
in {
"dreadnode-postgres",
"dreadnode-clickhouse",
"dreadnode-traefik",
"dreadnode-ui",
"dreadnode-api",
}
)
for service in required_services:
if service not in [c.name for c in container_details if c.status == "running"]:
return False
return True


def platform_status(tag: str | None = None) -> bool:
Expand Down