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
117 changes: 110 additions & 7 deletions dreadnode/cli/platform/cli.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
import typing as t

import cyclopts

from dreadnode.cli.platform.configure import configure_platform
from dreadnode.cli.platform.configure import configure_platform, list_configurations
from dreadnode.cli.platform.download import download_platform
from dreadnode.cli.platform.login import log_into_registries
from dreadnode.cli.platform.start import start_platform
from dreadnode.cli.platform.status import platform_status
from dreadnode.cli.platform.stop import stop_platform
from dreadnode.cli.platform.upgrade import upgrade_platform
from dreadnode.cli.platform.utils.printing import print_info
from dreadnode.cli.platform.utils.versions import get_current_version

cli = cyclopts.App("platform", help="Run and manage the platform.", help_flags=[])


@cli.command()
def start(tag: str | None = None) -> None:
def start(
tag: t.Annotated[
str | None, cyclopts.Parameter(help="Optional image tag to use when starting the platform.")
] = None,
**env_overrides: t.Annotated[
str,
cyclopts.Parameter(
help="Environment variable overrides. Use --key value format. "
"Examples: --proxy-host myproxy.local"
),
],
) -> None:
"""Start the platform. Optionally, provide a tagged version to start.

Args:
tag: Optional image tag to use when starting the platform.
**env_overrides: Key-value pairs to override environment variables in the
platform's .env file. e.g `--proxy-host myproxy.local`
"""
start_platform(tag=tag)
start_platform(tag=tag, **env_overrides)


@cli.command(name=["stop", "down"])
Expand All @@ -27,7 +45,11 @@ def stop() -> None:


@cli.command()
def download(tag: str | None = None) -> None:
def download(
tag: t.Annotated[
str | None, cyclopts.Parameter(help="Optional image tag to use when starting the platform.")
] = None,
) -> None:
"""Download platform files for a specific tag.

Args:
Expand All @@ -52,10 +74,91 @@ def refresh_registry_auth() -> None:


@cli.command()
def configure(service: str = "api") -> None:
def configure(
*args: t.Annotated[
str,
cyclopts.Parameter(
help="Key-value pairs to set. Must be provided in pairs (key value key value ...). ",
),
],
tag: t.Annotated[
str | None, cyclopts.Parameter(help="Optional image tag to use when starting the platform.")
] = None,
list: t.Annotated[
bool,
cyclopts.Parameter(
["--list", "-l"], help="List current configuration without making changes."
),
] = False,
unset: t.Annotated[
bool,
cyclopts.Parameter(["--unset", "-u"], help="Remove the specified configuration."),
] = False,
) -> None:
"""Configure the platform for a specific service.
Configurations will take effect the next time the platform is started and are persisted.

Usage: platform configure KEY VALUE [KEY2 VALUE2 ...]
Examples:
platform configure proxy-host myproxy.local
platform configure proxy-host myproxy.local api-port 8080

Args:
*args: Key-value pairs to set. Must be provided in pairs (key value key value ...).
tag: Optional image tag to use when starting the platform.
"""
if list:
if args:
raise ValueError("The --list option does not take any positional arguments.")
list_configurations()
return
# Parse positional arguments into key-value pairs
if not unset and len(args) % 2 != 0:
raise ValueError(
"Arguments must be provided in key-value pairs like: KEY VALUE [KEY2 VALUE2 ...]"
)

# Convert positional args to dict
env_overrides = {}
for i in range(0, len(args), 2):
key = args[i]
value = args[i + 1] if not unset else None
env_overrides[key] = value

configure_platform(tag=tag, **env_overrides)


@cli.command()
def version(
verbose: t.Annotated[ # noqa: FBT002
bool,
cyclopts.Parameter(
["--verbose", "-v"], help="Display detailed information for the version."
),
] = False,
) -> None:
"""Show the current platform version."""
version = get_current_version()
if version:
if verbose:
print_info(version.details)
else:
print_info(f"Current platform version: {version!s}")

else:
print_info("No current platform version is set.")


@cli.command()
def status(
tag: t.Annotated[
str | None, cyclopts.Parameter(help="Optional image tag to use when checking status.")
] = None,
) -> None:
"""Get the status of the platform with the specified or current version.

Args:
service: The name of the service to configure.
tag: Optional image tag to use. If not provided, uses the current
version or downloads the latest available version.
"""
configure_platform(service=service)
platform_status(tag=tag)
52 changes: 39 additions & 13 deletions dreadnode/cli/platform/configure.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
from dreadnode.cli.platform.utils.env_mgmt import open_env_file
from dreadnode.cli.platform.constants import SERVICES
from dreadnode.cli.platform.docker_ import build_docker_compose_override_file
from dreadnode.cli.platform.utils.env_mgmt import build_env_file, read_env_file
from dreadnode.cli.platform.utils.printing import print_info
from dreadnode.cli.platform.utils.versions import get_current_version, get_local_cache_dir
from dreadnode.cli.platform.utils.versions import get_current_version, get_local_version


def configure_platform(service: str = "api", tag: str | None = None) -> None:
def list_configurations() -> None:
"""List the current platform configuration overrides, if any."""
current_version = get_current_version()
if not current_version:
print_info("No current platform version is set. Please start or download the platform.")
return

overrides_env_file = current_version.configure_overrides_env_file
if not overrides_env_file.exists():
print_info("No configuration overrides found.")
return

print_info(f"Configuration overrides from {overrides_env_file}:")
env_vars = read_env_file(overrides_env_file)
for key, value in env_vars.items():
print_info(f" - {key}={value}")


def configure_platform(tag: str | None = None, **env_overrides: str | None) -> None:
"""Configure the platform for a specific service.

Args:
service: The name of the service to configure.
"""
if not tag:
current_version = get_current_version()
tag = current_version.tag if current_version else "latest"

print_info(f"Configuring {service} service...")
env_file = get_local_cache_dir() / tag / f".{service}.env"
open_env_file(env_file)
print_info(
f"Configuration for {service} service loaded. It will take effect the next time the service is started."
)
selected_version = get_local_version(tag) if tag else get_current_version()
# No need to mark current version on configure

if not selected_version:
print_info("No current platform version is set. Please start or download the platform.")
return

if env_overrides:
print_info("Setting environment overrides...")
build_docker_compose_override_file(SERVICES, selected_version)
build_env_file(selected_version.configure_overrides_env_file, **env_overrides)
print_info(
f"Configuration written to {selected_version.local_path}. "
"These will take effect the next time the platform is started."
" You can modify or remove them at any time."
)
9 changes: 6 additions & 3 deletions dreadnode/cli/platform/constants.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import typing as t

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

SupportedArchitecture = t.Literal["amd64", "arm64"]
SUPPORTED_ARCHITECTURES: list[SupportedArchitecture] = ["amd64", "arm64"]

DEFAULT_DOCKER_PROJECT_NAME = "dreadnode-platform"
Loading