Skip to content

Commit

Permalink
feat(cli): bot endpoints for cluster mgmt cli
Browse files Browse the repository at this point in the history
  • Loading branch information
fubuloubu committed Jul 19, 2024
1 parent 45d4007 commit 34074f6
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 4 deletions.
171 changes: 171 additions & 0 deletions silverback/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,50 @@ def bot():
"""Commands for managing bots in a CLUSTER"""


@bot.command(name="new")
@click.option("-n", "--name", required=True)
@click.option("-i", "--image", required=True)
@click.option("-n", "--network", required=True)
@click.option("-a", "--account")
@click.option("-g", "--group", "groups", multiple=True)
def new_bot(
client: ClusterClient,
name: str,
image: str,
network: str,
account: str | None,
groups: list[str],
):
"""Create a new bot in CLUSTER"""

if name in client.bots:
raise click.UsageError(f"Cannot use name '{name}' to create bot")

environment = list()
rendered_environment = dict()
for env_id in groups:
if "/" in env_id:
env_name, revision = env_id.split("/")
env = client.envs[env_name].revisions[int(revision)]

else:
env = client.envs[env_id]

environment.append(env)

for var_name in env.variables:
rendered_environment[var_name] = f"{env.name}/{env.revision}"

display = render_dict_as_yaml(rendered_environment, prepend="\n ")
click.echo(f"Environment:\n {display}")

if not click.confirm("Do you want to create this bot?"):
return

bot = client.new_bot(name, image, network, account=account, environment=environment)
click.secho(f"Bot ({bot.id}) deploying...", fg="green", bold=True)


@bot.command(name="list")
def list_bots(client: ClusterClient):
"""
Expand All @@ -406,3 +450,130 @@ def list_bots(client: ClusterClient):
click.secho("No bots in this cluster", bold=True, fg="red")


@bot.command(name="status")
@click.argument("bot_name", metavar="BOT")
def show_bot_status(client: ClusterClient, bot_name: str):
"""Show status of BOT in CLUSTER"""

if not (bot := client.bots.get(bot_name)):
raise click.UsageError(f"Unknown bot '{bot_name}'.")

click.echo(render_dict_as_yaml(bot))


@bot.command(name="update")
@click.option("-n", "--name", "new_name")
@click.option("-i", "--image")
@click.option("-n", "--network")
@click.option("-a", "--account")
@click.option("-g", "--group", "groups", multiple=True)
@click.argument("bot_name", metavar="BOT")
def update_bot(
client: ClusterClient,
bot_name: str,
new_name: str,
image: str,
network: str,
account: str | None,
groups: list[str],
):
"""Update configuration of BOT in CLUSTER"""

if new_name in client.bots:
raise click.UsageError(f"Cannot use name '{new_name}' to update bot '{bot_name}'")

if not (bot := client.bots.get(bot_name)):
raise click.UsageError(f"Unknown bot '{bot_name}'.")

environment = list()
rendered_environment = dict()
for env_id in groups:
if "/" in env_id:
env_name, revision = env_id.split("/")
env = client.envs[env_name].revisions[int(revision)]

else:
env = client.envs[env_id]

environment.append(env)

for var_name in env.variables:
rendered_environment[var_name] = f"{env.name}/{env.revision}"

set_environment = True

if len(environment) == 0:
set_environment = click.confirm("Do you want to clear all environment variables?")

else:
display = render_dict_as_yaml(rendered_environment, prepend="\n ")
click.echo(f"Environment:\n {display}")

if not click.confirm("Do you want to create this bot?"):
return

bot.update(
name=new_name,
image=image,
network=network,
account=account,
environment=environment if set_environment else None,
)


@bot.command(name="rm")
@click.argument("name", metavar="BOT")
def rm_bot(client: ClusterClient, name: str):
"""Remove BOT from CLUSTER"""

if not (bot := client.bots.get(name)):
raise click.UsageError(f"Unknown bot '{name}'.")

bot.remove()
click.secho(f"Bot '{bot.name}' removed.", fg="green", bold=True)


@bot.command(name="start")
@click.argument("name", metavar="BOT")
def start_bot(client: ClusterClient, name: str):
"""Start BOT running in CLUSTER (if stopped or terminated)"""

if not (bot := client.bots.get(name)):
raise click.UsageError(f"Unknown bot '{name}'.")

bot.start()


@bot.command(name="stop")
@click.argument("name", metavar="BOT")
def stop_bot(client: ClusterClient, name: str):
"""Stop BOT running in CLUSTER (if running)"""

if not (bot := client.bots.get(name)):
raise click.UsageError(f"Unknown bot '{name}'.")

bot.stop()


@bot.command(name="logs")
@click.argument("name", metavar="BOT")
def show_bot_logs(client: ClusterClient, name: str):
"""Show runtime logs for BOT in CLUSTER"""

if not (bot := client.bots.get(name)):
raise click.UsageError(f"Unknown bot '{name}'.")

for log in bot.logs:
click.echo(log)


@bot.command(name="errors")
@click.argument("name", metavar="BOT")
def show_bot_errors(client: ClusterClient, name: str):
"""Show errors for BOT in CLUSTER"""

if not (bot := client.bots.get(name)):
raise click.UsageError(f"Unknown bot '{name}'.")

for log in bot.errors:
click.echo(log)
85 changes: 81 additions & 4 deletions silverback/cluster/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import uuid
from functools import cache
from typing import ClassVar

Expand Down Expand Up @@ -70,13 +69,67 @@ def remove(self):
handle_error_with_response(response)


class Bot(BotInfo):
# NOTE: Client used only for this SDK
# NOTE: DI happens in `ClusterClient.__init__`
cluster: ClassVar["ClusterClient"]

def update(
self,
name: str | None = None,
image: str | None = None,
network: str | None = None,
account: str | None = None,
environment: list[EnvInfo] | None = None,
):
form: dict = dict(
name=name,
network=network,
account=account,
image=image,
)

if environment:
form["environment"] = [
dict(id=str(env.id), revision=env.revision) for env in environment
]

response = self.cluster.put(f"/bots/{self.id}", json=form)
handle_error_with_response(response)

def stop(self):
response = self.cluster.post(f"/bots/{self.id}/stop")
handle_error_with_response(response)

def start(self):
response = self.cluster.post(f"/bots/{self.id}/start")
handle_error_with_response(response)

@property
def errors(self) -> list[str]:
response = self.cluster.get(f"/bots/{self.id}/errors")
handle_error_with_response(response)
return response.json()

@property
def logs(self) -> list[str]:
response = self.cluster.get(f"/bots/{self.id}/logs")
handle_error_with_response(response)
return response.json()

def remove(self):
response = self.cluster.delete(f"/bots/{self.id}")
handle_error_with_response(response)


class ClusterClient(httpx.Client):
def __init__(self, *args, **kwargs):
kwargs["headers"] = {**kwargs.get("headers", {}), **DEFAULT_HEADERS}
super().__init__(*args, **kwargs)

# DI for other client classes
Env.cluster = self # Connect to cluster client
Bot.cluster = self # Connect to cluster client

def send(self, request, *args, **kwargs):
try:
Expand Down Expand Up @@ -108,10 +161,34 @@ def new_env(self, name: str, variables: dict[str, str]) -> EnvInfo:
return EnvInfo.model_validate(response.json())

@property
def bots(self) -> dict[str, BotInfo]:
response = self.get("/bot") # TODO: rename `/bots`
def bots(self) -> dict[str, Bot]:
response = self.get("/bots") # TODO: rename `/bots`
handle_error_with_response(response)
return {bot.slug: bot for bot in map(Bot.model_validate, response.json())}

def new_bot(
self,
name: str,
image: str,
network: str,
account: str | None = None,
environment: list[EnvInfo] | None = None,
) -> Bot:
form: dict = dict(
name=name,
image=image,
network=network,
account=account,
)

if environment is not None:
form["environment"] = [
dict(id=str(env.id), revision=env.revision) for env in environment
]

response = self.post("/bots", json=form)
handle_error_with_response(response)
return {bot.slug: bot for bot in map(BotInfo.model_validate, response.json())}
return Bot.model_validate(response.json())

def build_display_fields(self) -> dict[str, str | dict[str, str]]:
state = self.state
Expand Down

0 comments on commit 34074f6

Please sign in to comment.