diff --git a/docs/userguides/managing.md b/docs/userguides/managing.md index bbdcff10..574aba74 100644 --- a/docs/userguides/managing.md +++ b/docs/userguides/managing.md @@ -16,7 +16,7 @@ logging in to the Platform using [`silverback login`][silverback-login]. A Workspace is an area for one or more people to co-manage a set of clusters together. You can manage workspaces from the Silverback CLI using [`silverback cluster workspaces`][silverback-cluster-workspaces]. -Using the Silverback CLI you can [list workspaces][silverback-cluster-workspaces-list], [make new ones][silverback-cluster-workspaces-new], [view their configuration information][silverback-cluster-workspaces-info], [update their metadata][silverback-cluster-workspaces-update], as well as [delete them][silverback-cluster-workspaces-delete] workspaces. +Using the Silverback CLI you can [list workspaces][silverback-cluster-workspaces-list], [make new ones][silverback-cluster-workspaces-new], [view their configuration information][silverback-cluster-workspaces-info], [update their metadata][silverback-cluster-workspaces-update], as well as [delete them][silverback-cluster-workspaces-delete]. ## Managing a Cluster diff --git a/silverback/_cli.py b/silverback/_cli.py index 601981b6..60886504 100644 --- a/silverback/_cli.py +++ b/silverback/_cli.py @@ -228,39 +228,38 @@ def workspace_info(platform: "PlatformClient", workspace: str): "-n", "--name", "workspace_name", - required=True, help="Name for new workspace", ) @click.option( "-s", "--slug", "workspace_slug", - required=True, help="Slug for new workspace", ) @platform_client def new_workspace( platform: "PlatformClient", - workspace_name: str, - workspace_slug: str, + workspace_name: str | None, + workspace_slug: str | None, ): """Create a new workspace""" - if workspace_name: - click.echo(f"name: {workspace_name}") - click.echo(f"slug: {workspace_slug or workspace_name.lower().replace(' ', '-')}") - - elif workspace_slug: - click.echo(f"slug: {workspace_slug}") + workspace_name = workspace_name or workspace_slug + workspace_slug = workspace_slug or ( + workspace_name.lower().replace(" ", "-") if workspace_name else None + ) - else: + if not workspace_name: raise click.UsageError("Must provide a name or a slug/name combo") - platform.create_workspace( + workspace = platform.create_workspace( workspace_name=workspace_name, workspace_slug=workspace_slug, ) - click.echo(f"{click.style('SUCCESS', fg='green')}: Created '{workspace_name}'") + click.echo( + f"{click.style('SUCCESS', fg='green')}: " + f"Created '{workspace.name}' (slug: '{workspace.slug}')" + ) @workspaces.command(name="update", section="Platform Commands (https://silverback.apeworx.io)") @@ -330,8 +329,9 @@ def list_clusters(platform: "PlatformClient", workspace: str): if not (workspace_client := platform.workspaces.get(workspace)): raise click.BadOptionUsage("workspace", f"Unknown workspace '{workspace}'") - if cluster_names := list(workspace_client.clusters): - click.echo(yaml.safe_dump(cluster_names)) + if clusters := workspace_client.clusters.values(): + cluster_info = [f"- {cluster.name} ({cluster.status})" for cluster in clusters] + click.echo("\n".join(cluster_info)) else: click.secho("No clusters for this account", bold=True, fg="red") @@ -363,14 +363,12 @@ def new_cluster( if not (workspace_client := platform.workspaces.get(workspace)): raise click.BadOptionUsage("workspace", f"Unknown workspace '{workspace}'") - if cluster_name: - click.echo(f"name: {cluster_name}") - click.echo(f"slug: {cluster_slug or cluster_name.lower().replace(' ', '-')}") - - elif cluster_slug: - click.echo(f"slug: {cluster_slug}") + cluster_name = cluster_name or cluster_slug + cluster_slug = cluster_slug or ( + cluster_name.lower().replace(" ", "-") if cluster_name else None + ) - else: + if not cluster_name: raise click.UsageError("Must provide a name or a slug/name combo") from silverback.cluster.types import ResourceStatus @@ -379,7 +377,9 @@ def new_cluster( cluster_name=cluster_name, cluster_slug=cluster_slug, ) - click.echo(f"{click.style('SUCCESS', fg='green')}: Created '{cluster.name}'") + click.echo( + f"{click.style('SUCCESS', fg='green')}: Created '{cluster.name}' (slug: '{cluster.slug}')" + ) if cluster.status == ResourceStatus.CREATED: click.echo( @@ -658,7 +658,7 @@ def fund_payment_stream( ) elif cluster.status != ResourceStatus.RUNNING: - raise click.UsageError(f"Cannot fund '{cluster_info.name}': cluster is not running.") + raise click.UsageError(f"Cannot fund '{cluster.name}': cluster is not running.") elif not (stream := workspace_client.get_payment_stream(cluster, network.chain_id)): raise click.UsageError("Cluster is not funded via ApePay Stream") @@ -728,9 +728,6 @@ def cancel_payment_stream( f"Unknown cluster in workspace '{workspace_name}': '{cluster_name}'" ) - elif cluster.status != ResourceStatus.RUNNING: - raise click.UsageError(f"Cannot fund '{cluster_info.name}': cluster is not running.") - elif not (stream := workspace_client.get_payment_stream(cluster, network.chain_id)): raise click.UsageError("Cluster is not funded via ApePay Stream") @@ -749,12 +746,11 @@ def cluster_info(cluster: "ClusterClient"): # NOTE: This actually doesn't query the cluster's routes, which are protected click.echo(f"Cluster Version: v{cluster.version}") - - if config := cluster.state.configuration: - click.echo(yaml.safe_dump(config.settings_display_dict())) - - else: - click.secho("No Cluster Configuration detected", fg="yellow", bold=True) + # TODO: Add way to fetch config and display it (this doesn't work) + # if config := cluster.state.configuration: + # click.echo(yaml.safe_dump(config.settings_display_dict())) + # else: + # click.secho("No Cluster Configuration detected", fg="yellow", bold=True) @cluster.command(name="health") diff --git a/silverback/cluster/client.py b/silverback/cluster/client.py index ff4cfc5b..708da4ea 100644 --- a/silverback/cluster/client.py +++ b/silverback/cluster/client.py @@ -9,6 +9,7 @@ from apepay import Stream, StreamManager from pydantic import computed_field +from silverback.exceptions import ClientError from silverback.version import version from .types import ( @@ -54,7 +55,7 @@ def render_error(error: dict): else: message = response.text - raise RuntimeError(message) + raise ClientError(message) response.raise_for_status() @@ -480,8 +481,8 @@ def workspaces(self) -> dict[str, Workspace]: def create_workspace( self, - workspace_slug: str = "", - workspace_name: str = "", + workspace_slug: str | None = None, + workspace_name: str | None = None, ) -> Workspace: response = self.post( "/workspaces", diff --git a/silverback/exceptions.py b/silverback/exceptions.py index 554f5a88..ffc6c6d3 100644 --- a/silverback/exceptions.py +++ b/silverback/exceptions.py @@ -1,5 +1,6 @@ from typing import Any +import click from ape.exceptions import ApeException from .types import TaskType @@ -41,6 +42,11 @@ def __init__(self, *exceptions: Exception | str): super().__init__("Startup failure(s) detected. See logs for details.") +# NOTE: Subclass `click.UsageError` here so bad requests in CLI don't show stack trace +class ClientError(SilverbackException, click.UsageError): + """Exception for client errors in the HTTP request.""" + + class NoTasksAvailableError(SilverbackException): def __init__(self): super().__init__("No tasks to execute")