diff --git a/silverback/_cli.py b/silverback/_cli.py index c3381bec..50cd2704 100644 --- a/silverback/_cli.py +++ b/silverback/_cli.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from typing import TYPE_CHECKING, Optional +from silverback.cluster.client import RegistryCredentials import click import yaml # type: ignore[import-untyped] @@ -789,8 +790,8 @@ def registry(): @cluster_client def credentials_list(cluster: "ClusterClient"): """List container registry credentials""" - - if creds := list(cluster.registry_credentials): + + if creds := cluster.registry_credentials: click.echo(yaml.safe_dump(creds)) else: @@ -814,17 +815,22 @@ def credentials_info(cluster: "ClusterClient", name: str): @click.argument("registry") @cluster_client def credentials_new(cluster: "ClusterClient", name: str, registry: str): - """Add registry private registry credentials. This command will prompt you for a username and - password. + """Add registry private registry credentials. This command will prompt you for a username, + password, and email. """ username = click.prompt("Username") password = click.prompt("Password", hide_input=True) + email = click.prompt("Email") creds = cluster.new_credentials( - name=name, hostname=registry, username=username, password=password + name=name, + docker_server=registry, + docker_username=username, + docker_password=password, + docker_email=email ) - click.echo(yaml.safe_dump(creds.model_dump(exclude={"id"}))) + click.echo(yaml.safe_dump(creds.model_dump())) @registry.command(name="update") @@ -838,9 +844,15 @@ def credentials_update(cluster: "ClusterClient", name: str, registry: str | None username = click.prompt("Username") password = click.prompt("Password", hide_input=True) + email = click.prompt("Email") - creds = creds.update(hostname=registry, username=username, password=password) - click.echo(yaml.safe_dump(creds.model_dump(exclude={"id"}))) + creds = creds.update( + docker_server=registry, + docker_username=username, + docker_password=password, + docker_email=email + ) + click.echo(yaml.safe_dump(creds.model_dump())) @registry.command(name="remove") @@ -848,12 +860,20 @@ def credentials_update(cluster: "ClusterClient", name: str, registry: str | None @cluster_client def credentials_remove(cluster: "ClusterClient", name: str): """Remove a set of registry credentials""" - if not (creds := cluster.registry_credentials.get(name)): + # Verify the credential exists in the list + if name not in cluster.registry_credentials: raise click.UsageError(f"Unknown credentials '{name}'") - creds.remove() # NOTE: No confirmation because can only delete if no references exist - click.secho(f"registry credentials '{creds.name}' removed.", fg="green", bold=True) - + # Create instance and remove + creds = RegistryCredentials( + name=name, + docker_server="", + docker_username="", + docker_password="", + docker_email="" + ) + creds.remove() # This will call DELETE /credentials/{name} + click.secho(f"registry credentials '{name}' removed.", fg="green", bold=True) @cluster.group(cls=SectionedHelpGroup) def vars(): diff --git a/silverback/cluster/client.py b/silverback/cluster/client.py index 2392662b..8dc12ccf 100644 --- a/silverback/cluster/client.py +++ b/silverback/cluster/client.py @@ -8,7 +8,7 @@ from ape.contracts import ContractInstance from ape.logging import LogLevel from apepay import Stream, StreamManager -from pydantic import computed_field +from pydantic import computed_field, ValidationError from silverback.exceptions import ClientError from silverback.version import version @@ -74,19 +74,20 @@ def __hash__(self) -> int: def update( self, name: str | None = None, - hostname: str | None = None, - username: str | None = None, - password: str | None = None, + docker_server: str | None = None, + docker_username: str | None = None, + docker_password: str | None = None, + docker_email: str | None = None, ) -> "RegistryCredentials": response = self.cluster.put( - f"/credentials/{self.id}", - json=dict(name=name, hostname=hostname, username=username, password=password), + f"/credentials/{self.name}", + json=dict(name=name, docker_server=docker_server, docker_username=docker_username, docker_password=docker_password, docker_email=docker_email), ) handle_error_with_response(response) return self def remove(self): - response = self.cluster.delete(f"/credentials/{self.id}") + response = self.cluster.delete(f"/credentials/{self.name}") handle_error_with_response(response) @@ -215,6 +216,7 @@ def logs(self) -> list[BotLogEntry]: def remove(self, network: str): response = self.cluster.delete(f"/bots/{self.name}", params={"network": network}) + breakpoint() handle_error_with_response(response) @@ -230,6 +232,7 @@ def __init__(self, *args, **kwargs): RegistryCredentials.cluster = self # Connect to cluster client VariableGroup.cluster = self # Connect to cluster client Bot.cluster = self # Connect to cluster client + def send(self, request, *args, **kwargs): try: @@ -262,24 +265,39 @@ def health(self) -> ClusterHealth: handle_error_with_response(response) return ClusterHealth.model_validate(response.json()) + def get_credential_details(self, name: str) -> RegistryCredentialsInfo: + # Try to get full details of a single credential + response = self.get(f"/credentials/{name}") + handle_error_with_response(response) + return RegistryCredentialsInfo.model_validate(response.json()) + @property def registry_credentials(self) -> dict[str, RegistryCredentials]: response = self.get("/credentials") handle_error_with_response(response) + return { - creds.name: creds for creds in map(RegistryCredentials.model_validate, response.json()) - } + name: self.get_credential_details(name) # We'd implement this to use the right HTTP method + for name in response.json() + } def new_credentials( - self, name: str, hostname: str, username: str, password: str + self, name: str, docker_server: str, docker_username: str, docker_password: str, docker_email: str ) -> RegistryCredentials: - response = self.post( - "/credentials", - json=dict(name=name, hostname=hostname, username=username, password=password), - ) + form = dict( + name=name, + docker_server=docker_server, + docker_username=docker_username, + docker_password=docker_password, + docker_email=docker_email, + ) + response = self.post("/credentials", json=form) handle_error_with_response(response) - return RegistryCredentials.model_validate(response.json()) - + try: + return RegistryCredentials.model_validate(response.json()) + except ValidationError as e: + raise ClientError(f"Invalid response format: {e}") + @property def variable_groups(self) -> dict[str, VariableGroup]: response = self.get("/vars") diff --git a/silverback/cluster/types.py b/silverback/cluster/types.py index 4eb8b9b6..fbcca092 100644 --- a/silverback/cluster/types.py +++ b/silverback/cluster/types.py @@ -328,11 +328,11 @@ def cluster(self) -> ServiceHealth: class RegistryCredentialsInfo(BaseModel): - id: str name: str - hostname: str - created: datetime - updated: datetime + docker_server: str + docker_username: str + docker_password: str + docker_email: str class VariableGroupInfo(BaseModel):