"vscode:/vscode.git/clone" did not exist on "4c38680eac53be3edc9813ebca5a2517baabba06"
Unverified Commit c0bdf412 authored by hhzhang16's avatar hhzhang16 Committed by GitHub
Browse files

feat: improve dynamo deployment CLI (#798)


Co-authored-by: default avatarJulien Mancuso <jmancuso@nvidia.com>
parent 4761baa6
...@@ -112,7 +112,7 @@ async def create_deployment(deployment: CreateDeploymentSchema): ...@@ -112,7 +112,7 @@ async def create_deployment(deployment: CreateDeploymentSchema):
deployment_schema = DeploymentFullSchema( deployment_schema = DeploymentFullSchema(
**resource.dict(), **resource.dict(),
status="running", status="deploying",
kube_namespace=kube_namespace, kube_namespace=kube_namespace,
creator=creator, creator=creator,
cluster=cluster, cluster=cluster,
...@@ -159,21 +159,53 @@ def get_deployment(name: str) -> DeploymentFullSchema: ...@@ -159,21 +159,53 @@ def get_deployment(name: str) -> DeploymentFullSchema:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
# function to look for a condition with type "Ready" in the status of the deployment
# and return the "message" field
def get_deployment_status(resource: Dict[str, Any]) -> str: def get_deployment_status(resource: Dict[str, Any]) -> str:
# look for a condition with type "Ready" in the status of the deployment """
for condition in resource.get("status", {}).get("conditions", []): Get the current status of a deployment.
Maps operator status to BentoML status values.
Returns lowercase status values matching BentoML's DeploymentStatus enum.
"""
status = resource.get("status", {})
conditions = status.get("conditions", [])
state = status.get("state", "")
# First check Ready condition
for condition in conditions:
if condition.get("type") == "Ready": if condition.get("type") == "Ready":
return condition.get("message", "unknown") if condition.get("status") == "True":
# If state is "successful", map to "running"
if state == "successful":
return "running"
return condition.get("message", "running").lower()
elif condition.get("message"):
return condition.get("message").lower()
# If no Ready condition or not True, check state
if state == "failed":
return "failed"
elif state == "pending":
return "deploying" # map pending to deploying to match BentoML states
# Default fallback
return "unknown" return "unknown"
def get_urls(resource: Dict[str, Any]) -> List[str]: def get_urls(resource: Dict[str, Any]) -> List[str]:
"""
Get the URLs for a deployment.
Returns URLs as soon as they are available from EndpointExposed condition.
"""
urls = [] urls = []
for condition in resource.get("status", {}).get("conditions", []): conditions = resource.get("status", {}).get("conditions", [])
if condition.get("type") == "EndpointExposed":
urls.append(condition.get("message")) # Check for EndpointExposed condition
for condition in conditions:
if (
condition.get("type") == "EndpointExposed"
and condition.get("status") == "True"
):
if message := condition.get("message"):
urls.append(message)
return urls return urls
......
...@@ -32,10 +32,8 @@ def create_bentoml_cli() -> click.Command: ...@@ -32,10 +32,8 @@ def create_bentoml_cli() -> click.Command:
from dynamo.sdk.cli.bentos import bento_command from dynamo.sdk.cli.bentos import bento_command
from dynamo.sdk.cli.cloud import cloud_command from dynamo.sdk.cli.cloud import cloud_command
from dynamo.sdk.cli.deployment import deployment_command from dynamo.sdk.cli.deployment import deploy_command, deployment_command
from dynamo.sdk.cli.env import env_command from dynamo.sdk.cli.env import env_command
# from dynamo.sdk.cli.deploy import deploy_command
from dynamo.sdk.cli.run import run_command from dynamo.sdk.cli.run import run_command
from dynamo.sdk.cli.serve import serve_command from dynamo.sdk.cli.serve import serve_command
from dynamo.sdk.cli.utils import DynamoCommandGroup from dynamo.sdk.cli.utils import DynamoCommandGroup
...@@ -63,7 +61,7 @@ def create_bentoml_cli() -> click.Command: ...@@ -63,7 +61,7 @@ def create_bentoml_cli() -> click.Command:
bentoml_cli.add_single_command(bento_command, "get") bentoml_cli.add_single_command(bento_command, "get")
bentoml_cli.add_subcommands(serve_command) bentoml_cli.add_subcommands(serve_command)
bentoml_cli.add_subcommands(run_command) bentoml_cli.add_subcommands(run_command)
# bentoml_cli.add_command(deploy_command) bentoml_cli.add_command(deploy_command)
# bentoml_cli.add_command(containerize_command) # bentoml_cli.add_command(containerize_command)
bentoml_cli.add_command(deployment_command) bentoml_cli.add_command(deployment_command)
bentoml_cli.add_command(env_command) bentoml_cli.add_command(env_command)
......
...@@ -37,6 +37,9 @@ from dynamo.sdk.lib.logging import configure_server_logging ...@@ -37,6 +37,9 @@ from dynamo.sdk.lib.logging import configure_server_logging
from .utils import resolve_service_config from .utils import resolve_service_config
# Configure logging to suppress INFO HTTP logs
logging.getLogger("httpx").setLevel(logging.WARNING) # HTTP client library logs
logging.getLogger("httpcore").setLevel(logging.WARNING) # HTTP core library logs
configure_server_logging() configure_server_logging()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -264,7 +267,6 @@ def create_deployment( ...@@ -264,7 +267,6 @@ def create_deployment(
) -> Deployment: ) -> Deployment:
# Load config from file and serialize to env # Load config from file and serialize to env
service_configs = resolve_service_config(config_file=config_file, args=args) service_configs = resolve_service_config(config_file=config_file, args=args)
print(f"service_configs: {service_configs}")
env_dicts = [] env_dicts = []
if service_configs: if service_configs:
config_json = json.dumps(service_configs) config_json = json.dumps(service_configs)
...@@ -288,39 +290,80 @@ def create_deployment( ...@@ -288,39 +290,80 @@ def create_deployment(
console = Console(highlight=False) console = Console(highlight=False)
with Spinner(console=console) as spinner: with Spinner(console=console) as spinner:
spinner.update("Creating deployment on Dynamo Cloud")
try: try:
# Create deployment with initial status message
spinner.update("Creating deployment on Dynamo Cloud...")
deployment = _cloud_client.deployment.create( deployment = _cloud_client.deployment.create(
deployment_config_params=config_params deployment_config_params=config_params
) )
deployment.admin_console = _get_urls(deployment) # remove dashboard url
spinner.log( spinner.log(
f':white_check_mark: Created deployment "{deployment.name}" in cluster "{deployment.cluster}"' f':white_check_mark: Created deployment "{deployment.name}" in cluster "{deployment.cluster}"'
) )
if wait: if wait:
spinner.update( # Update spinner text for waiting phase
"[bold blue]Waiting for deployment to be ready, you can use --no-wait to skip this process[/]", spinner.log(
"[bold blue]Waiting for deployment to be ready, you can use --no-wait to skip this process[/]"
) )
retcode = deployment.wait_until_ready(timeout=timeout, spinner=spinner) retcode = deployment.wait_until_ready(timeout=timeout, spinner=spinner)
if retcode != 0: if retcode != 0:
sys.exit(retcode) sys.exit(retcode)
_display_deployment_info(spinner, deployment)
return deployment return deployment
except BentoMLException as e: except BentoMLException as e:
error_msg = str(e) error_msg = str(e)
if "already exists" in error_msg: if "already exists" in error_msg:
# Extract deployment name from error message and clean it # Extract deployment name from error message and clean it
match = re.search(r'"([^"]+?)(?:\\+)?" already exists', error_msg) match = re.search(r'"([^"]+?)(?:\\+)?" already exists', error_msg)
dep_name = match.group(1).rstrip("\\") if match else name dep_name = match.group(1).rstrip("\\") if match else name
error_msg = ( spinner.log(
f'Error: Deployment "{dep_name}" already exists. To create a new deployment:\n' "[red]:x: Error:[/] "
f"1. Use a different name with the --name flag\n" f'Deployment "{dep_name}" already exists. To create a new deployment:\n'
f"2. Or delete the existing deployment with: dynamo deployment delete {dep_name}" " 1. Use a different name with the --name flag\n"
f" 2. Or delete the existing deployment with: dynamo deployment delete {dep_name}"
) )
print(error_msg)
sys.exit(1) sys.exit(1)
print(f"Error: {str(e)}") spinner.log(f"[red]:x: Error:[/] {str(e)}")
sys.exit(1) sys.exit(1)
def _get_urls(deployment: Deployment) -> list[str]:
"""Get URLs from deployment."""
latest = deployment._client.v2.get_deployment(deployment.name, deployment.cluster)
urls = latest.urls if hasattr(latest, "urls") else None
return urls if urls is not None else []
def _display_deployment_info(spinner: Spinner, deployment: Deployment) -> None:
"""Helper function to display deployment status and URLs consistently."""
# Get status directly from schema and escape any Rich markup
status = deployment._schema.status if deployment._schema.status else "unknown"
# Escape any characters that are interpreted as markup
reformatted_status = status.replace("[", "\\[")
spinner.log(f"[bold]Status:[/] {reformatted_status}")
# Get URLs directly from schema
spinner.log("[bold]Ingress URLs:[/]")
try:
# Get latest deployment info for URLs
urls = _get_urls(deployment)
if urls:
for url in urls:
spinner.log(f" - {url}")
else:
spinner.log(" No URLs available")
except Exception:
# If refresh fails, fall back to existing URLs
if deployment._urls:
for url in deployment._urls:
spinner.log(f" - {url}")
else:
spinner.log(" No URLs available")
@inject @inject
def get_deployment( def get_deployment(
name: str, name: str,
...@@ -330,19 +373,27 @@ def get_deployment( ...@@ -330,19 +373,27 @@ def get_deployment(
"""Get deployment details from Dynamo Cloud.""" """Get deployment details from Dynamo Cloud."""
console = Console(highlight=False) console = Console(highlight=False)
with Spinner(console=console) as spinner: with Spinner(console=console) as spinner:
spinner.update(f'Getting deployment "{name}" from Dynamo Cloud')
try: try:
spinner.update(f'Getting deployment "{name}" from Dynamo Cloud...')
deployment = _cloud_client.deployment.get(name=name, cluster=cluster) deployment = _cloud_client.deployment.get(name=name, cluster=cluster)
spinner.log( spinner.log(
f':white_check_mark: Found deployment "{deployment.name}" in cluster "{deployment.cluster}"' f':white_check_mark: Found deployment "{deployment.name}" in cluster "{deployment.cluster}"'
) )
_display_deployment_info(spinner, deployment)
return deployment return deployment
except BentoMLException as e: except BentoMLException as e:
if "No cloud context default found" in str(e): error_msg = str(e)
raise BentoMLException( if "No cloud context default found" in error_msg:
"Not logged in to Dynamo Cloud. Please run 'dynamo cloud login' first." spinner.log(
) from None "[red]:x: Error:[/] Not logged in to Dynamo Cloud. Please run 'dynamo cloud login' first."
raise_deployment_config_error(e, "get") )
sys.exit(1)
if "404 Not Found" in error_msg or "Deployment not found" in error_msg:
cluster_msg = f" in cluster {cluster}" if cluster else ""
spinner.log(f"[red]:x: Deployment '{name}' not found{cluster_msg}")
sys.exit(1)
spinner.log(f"[red]:x: Error:[/] Failed to get deployment: {error_msg}")
sys.exit(1)
@inject @inject
...@@ -354,16 +405,23 @@ def delete_deployment( ...@@ -354,16 +405,23 @@ def delete_deployment(
"""Delete a deployment from Dynamo Cloud.""" """Delete a deployment from Dynamo Cloud."""
console = Console(highlight=False) console = Console(highlight=False)
with Spinner(console=console) as spinner: with Spinner(console=console) as spinner:
spinner.update(f'Deleting deployment "{name}" from Dynamo Cloud')
try: try:
spinner.update(f'Deleting deployment "{name}" from Dynamo Cloud...')
_cloud_client.deployment.delete(name=name, cluster=cluster) _cloud_client.deployment.delete(name=name, cluster=cluster)
spinner.log(f':white_check_mark: Deleted deployment "{name}"') spinner.log(f':white_check_mark: Successfully deleted deployment "{name}"')
except BentoMLException as e: except BentoMLException as e:
if "No cloud context default found" in str(e): error_msg = str(e)
raise BentoMLException( if "No cloud context default found" in error_msg:
"Not logged in to Dynamo Cloud. Please run 'dynamo cloud login' first." spinner.log(
) from None "[red]:x: Error:[/] Not logged in to Dynamo Cloud. Please run 'dynamo cloud login' first."
raise_deployment_config_error(e, "delete") )
sys.exit(1)
if "404 Not Found" in error_msg or "Deployment not found" in error_msg:
cluster_msg = f" in cluster {cluster}" if cluster else ""
spinner.log(f"[red]:x: Deployment '{name}' not found{cluster_msg}")
sys.exit(1)
spinner.log(f"[red]:x: Error:[/] {error_msg}")
sys.exit(1)
@inject @inject
...@@ -378,7 +436,6 @@ def list_deployments( ...@@ -378,7 +436,6 @@ def list_deployments(
"""List all deployments from Dynamo Cloud.""" """List all deployments from Dynamo Cloud."""
console = Console(highlight=False) console = Console(highlight=False)
with Spinner(console=console) as spinner: with Spinner(console=console) as spinner:
spinner.update("Getting all deployments from Dynamo Cloud")
try: try:
# Handle label-based filtering # Handle label-based filtering
if labels is not None: if labels is not None:
...@@ -388,6 +445,8 @@ def list_deployments( ...@@ -388,6 +445,8 @@ def list_deployments(
else: else:
q = label_query q = label_query
spinner.update("Getting deployments from Dynamo Cloud...")
# Get all deployments in a single call by setting count=1000
deployments = _cloud_client.deployment.list( deployments = _cloud_client.deployment.list(
cluster=cluster, search=search, dev=dev, q=q cluster=cluster, search=search, dev=dev, q=q
) )
...@@ -398,10 +457,13 @@ def list_deployments( ...@@ -398,10 +457,13 @@ def list_deployments(
spinner.log(":white_check_mark: Found deployments:") spinner.log(":white_check_mark: Found deployments:")
for deployment in deployments: for deployment in deployments:
spinner.log(f" • {deployment.name} (cluster: {deployment.cluster})") spinner.log(f"\n{deployment.name} (cluster: {deployment.cluster})")
_display_deployment_info(spinner, deployment)
except BentoMLException as e: except BentoMLException as e:
if "No cloud context default found" in str(e): if "No cloud context default found" in str(e):
raise BentoMLException( spinner.log(
"Not logged in to Dynamo Cloud. Please run 'dynamo cloud login' first." "[red]:x: Error:[/] Not logged in to Dynamo Cloud. Please run 'dynamo cloud login' first."
) from None )
raise_deployment_config_error(e, "list") sys.exit(1)
spinner.log(f"[red]:x: Error:[/] Failed to list deployments: {str(e)}")
sys.exit(1)
...@@ -164,7 +164,7 @@ DYNAMO_TAG=$(dynamo build hello_world:Frontend | grep "Successfully built" | awk ...@@ -164,7 +164,7 @@ DYNAMO_TAG=$(dynamo build hello_world:Frontend | grep "Successfully built" | awk
```bash ```bash
echo $DYNAMO_TAG echo $DYNAMO_TAG
export HELM_RELEASE=ci-hw export HELM_RELEASE=ci-hw
dynamo deployment create $DYNAMO_TAG --no-wait -n $HELM_RELEASE dynamo deployment create $DYNAMO_TAG -n $HELM_RELEASE
``` ```
4. **Test the deployment** 4. **Test the deployment**
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment