Skip to content

Commit 5d99ab3

Browse files
authored
Merge pull request #158 from dreadnode/brian/eng-2922-feat-add-platform-download-capability-to-sdk
feat: add platform download capability to sdk
2 parents e545e2e + 902cb82 commit 5d99ab3

File tree

21 files changed

+1491
-5
lines changed

21 files changed

+1491
-5
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
],
1616
"python.testing.unittestEnabled": false,
1717
"python.testing.pytestEnabled": true
18-
}
18+
}

dreadnode/api/client.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515

1616
from dreadnode.api.models import (
1717
AccessRefreshTokenResponse,
18+
ContainerRegistryCredentials,
1819
DeviceCodeResponse,
1920
ExportFormat,
2021
GithubTokenResponse,
2122
MetricAggregationType,
2223
Project,
2324
RawRun,
2425
RawTask,
26+
RegistryImageDetails,
2527
Run,
2628
RunSummary,
2729
StatusFilter,
@@ -710,3 +712,55 @@ def get_user_data_credentials(self) -> UserDataCredentials:
710712
"""
711713
response = self._request("GET", "/user-data/credentials")
712714
return UserDataCredentials(**response.json())
715+
716+
# Container registry access
717+
718+
def get_container_registry_credentials(self) -> ContainerRegistryCredentials:
719+
"""
720+
Retrieves container registry credentials for Docker image access.
721+
722+
Returns:
723+
The container registry credentials object.
724+
"""
725+
response = self.request("POST", "/platform/registry-token")
726+
return ContainerRegistryCredentials(**response.json())
727+
728+
def get_platform_releases(
729+
self, tag: str, services: list[str], cli_version: str | None
730+
) -> RegistryImageDetails:
731+
"""
732+
Resolves the platform releases for the current project.
733+
734+
Returns:
735+
The resolved platform releases as a ResolveReleasesResponse object.
736+
"""
737+
payload = {
738+
"tag": tag,
739+
"services": services,
740+
"cli_version": cli_version,
741+
}
742+
try:
743+
response = self.request("POST", "/platform/get-releases", json_data=payload)
744+
745+
except RuntimeError as e:
746+
if "403" in str(e):
747+
raise RuntimeError("You do not have access to platform releases.") from e
748+
749+
if "404" in str(e):
750+
if "Image not found" in str(e):
751+
raise RuntimeError("Image not found") from e
752+
753+
raise RuntimeError(
754+
f"Failed to get platform releases: {e}. The feature is likely disabled on this server"
755+
) from e
756+
raise
757+
return RegistryImageDetails(**response.json())
758+
759+
def get_platform_templates(self, tag: str) -> bytes:
760+
"""
761+
Retrieves the available platform templates.
762+
"""
763+
params = {"tag": tag}
764+
response = self.request("GET", "/platform/templates/all", params=params)
765+
zip_content: bytes = response.content
766+
return zip_content

dreadnode/api/models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,33 @@ class UserDataCredentials(BaseModel):
4343
endpoint: str | None
4444

4545

46+
class ContainerRegistryCredentials(BaseModel):
47+
registry: str
48+
username: str
49+
password: str
50+
expires_at: datetime
51+
52+
53+
class PlatformImage(BaseModel):
54+
service: str
55+
uri: str
56+
digest: str
57+
tag: str
58+
59+
@property
60+
def full_uri(self) -> str:
61+
return f"{self.uri}@{self.digest}"
62+
63+
@property
64+
def registry(self) -> str:
65+
return self.uri.split("/")[0]
66+
67+
68+
class RegistryImageDetails(BaseModel):
69+
tag: str
70+
images: list[PlatformImage]
71+
72+
4673
# Auth
4774

4875

dreadnode/cli/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
download_and_unzip_archive,
2121
validate_server_for_clone,
2222
)
23+
from dreadnode.cli.platform import cli as platform_cli
2324
from dreadnode.cli.profile import cli as profile_cli
2425
from dreadnode.constants import DEBUG, PLATFORM_BASE_URL
2526
from dreadnode.user_config import ServerConfig, UserConfig
@@ -28,8 +29,9 @@
2829

2930
cli["--help"].group = "Meta"
3031

31-
cli.command(profile_cli)
3232
cli.command(agent_cli)
33+
cli.command(platform_cli)
34+
cli.command(profile_cli)
3335

3436

3537
@cli.meta.default

dreadnode/cli/platform/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from dreadnode.cli.platform.cli import cli
2+
3+
__all__ = ["cli"]

dreadnode/cli/platform/cli.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import cyclopts
2+
3+
from dreadnode.cli.platform.configure import configure_platform
4+
from dreadnode.cli.platform.download import download_platform
5+
from dreadnode.cli.platform.login import log_into_registries
6+
from dreadnode.cli.platform.start import start_platform
7+
from dreadnode.cli.platform.stop import stop_platform
8+
from dreadnode.cli.platform.upgrade import upgrade_platform
9+
10+
cli = cyclopts.App("platform", help="Run and manage the platform.", help_flags=[])
11+
12+
13+
@cli.command()
14+
def start(tag: str | None = None) -> None:
15+
"""Start the platform. Optionally, provide a tagged version to start.
16+
17+
Args:
18+
tag: Optional image tag to use when starting the platform.
19+
"""
20+
start_platform(tag=tag)
21+
22+
23+
@cli.command(name=["stop", "down"])
24+
def stop() -> None:
25+
"""Stop the running platform."""
26+
stop_platform()
27+
28+
29+
@cli.command()
30+
def download(tag: str | None = None) -> None:
31+
"""Download platform files for a specific tag.
32+
33+
Args:
34+
tag: Optional image tag to download.
35+
"""
36+
download_platform(tag=tag)
37+
38+
39+
@cli.command()
40+
def upgrade() -> None:
41+
"""Upgrade the platform to the latest version."""
42+
upgrade_platform()
43+
44+
45+
@cli.command()
46+
def refresh_registry_auth() -> None:
47+
"""Refresh container registry credentials for platform access.
48+
49+
Used for out of band Docker management.
50+
"""
51+
log_into_registries()
52+
53+
54+
@cli.command()
55+
def configure(service: str = "api") -> None:
56+
"""Configure the platform for a specific service.
57+
58+
Args:
59+
service: The name of the service to configure.
60+
"""
61+
configure_platform(service=service)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from dreadnode.cli.platform.utils.env_mgmt import open_env_file
2+
from dreadnode.cli.platform.utils.printing import print_info
3+
from dreadnode.cli.platform.utils.versions import get_current_version, get_local_cache_dir
4+
5+
6+
def configure_platform(service: str = "api", tag: str | None = None) -> None:
7+
"""Configure the platform for a specific service.
8+
9+
Args:
10+
service: The name of the service to configure.
11+
"""
12+
if not tag:
13+
current_version = get_current_version()
14+
tag = current_version.tag if current_version else "latest"
15+
16+
print_info(f"Configuring {service} service...")
17+
env_file = get_local_cache_dir() / tag / f".{service}.env"
18+
open_env_file(env_file)
19+
print_info(
20+
f"Configuration for {service} service loaded. It will take effect the next time the service is started."
21+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import typing as t
2+
3+
API_SERVICE = "api"
4+
UI_SERVICE = "ui"
5+
SERVICES = [API_SERVICE, UI_SERVICE]
6+
VERSIONS_MANIFEST = "versions.json"
7+
8+
SupportedArchitecture = t.Literal["amd64", "arm64"]
9+
SUPPORTED_ARCHITECTURES: list[SupportedArchitecture] = ["amd64", "arm64"]

0 commit comments

Comments
 (0)