diff --git a/riocli/deployment/delete.py b/riocli/deployment/delete.py index 37839961..8cb0a661 100644 --- a/riocli/deployment/delete.py +++ b/riocli/deployment/delete.py @@ -11,12 +11,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import functools +from concurrent.futures import ThreadPoolExecutor +from queue import Queue + import click from click_help_colors import HelpColorsCommand +from rapyuta_io.clients.deployment import Deployment from riocli.config import new_client from riocli.constants import Colors, Symbols -from riocli.deployment.util import name_to_guid +from riocli.deployment.util import fetch_deployments +from riocli.deployment.util import print_deployments_for_confirmation +from riocli.utils import tabulate_data from riocli.utils.spinner import with_spinner @@ -28,33 +35,93 @@ ) @click.option('--force', '-f', '--silent', is_flag=True, default=False, help='Skip confirmation') -@click.argument('deployment-name', type=str) -@name_to_guid +@click.option('--all', 'delete_all', is_flag=True, default=False, + help='Deletes all deployments in the project') +@click.option('--workers', '-w', + help="Number of parallel workers while running delete deployment " + "command. Defaults to 10.", type=int, default=10) +@click.argument('deployment-name-or-regex', type=str, default="") @with_spinner(text="Deleting deployment...") def delete_deployment( force: bool, - deployment_name: str, - deployment_guid: str, + deployment_name_or_regex: str, + delete_all: bool = False, + workers: int = 10, spinner=None, ) -> None: """ - Deletes a deployment + Deletes one or more deployments given a name or a pattern """ + client = new_client() + + if not (deployment_name_or_regex or delete_all): + spinner.text = "Nothing to delete" + spinner.green.ok(Symbols.SUCCESS) + return + + try: + deployments = fetch_deployments( + client, deployment_name_or_regex, delete_all) + except Exception as e: + spinner.text = click.style( + 'Failed to delete deployment(s): {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e + + if not deployments: + spinner.text = "Nothing to delete" + spinner.ok(Symbols.SUCCESS) + return + with spinner.hidden(): - if not force: - click.confirm( - 'Deleting {} ({}) deployment'.format( - deployment_name, deployment_guid), abort=True) + print_deployments_for_confirmation(deployments) + + spinner.write('') + + if not force: + with spinner.hidden(): + click.confirm('Do you want to delete the above deployment(s)?', + default=True, abort=True) + spinner.write('') try: - client = new_client() - deployment = client.get_deployment(deployment_guid) - deployment.deprovision() + result = Queue() + func = functools.partial(_apply_delete, result) + with ThreadPoolExecutor(max_workers=workers) as executor: + executor.map(func, deployments) + + result = sorted(list(result.queue), key=lambda x: x[0]) + data, statuses = [], [] + for name, status in result: + fg = Colors.GREEN if status else Colors.RED + icon = Symbols.SUCCESS if status else Symbols.ERROR + statuses.append(status) + data.append([ + click.style(name, fg), + click.style(icon, fg) + ]) + + with spinner.hidden(): + tabulate_data(data, headers=['Name', 'Status']) + + icon = Symbols.SUCCESS if all(statuses) else Symbols.WARNING + fg = Colors.GREEN if all(statuses) else Colors.YELLOW + text = "successfully" if all(statuses) else "partially" + + spinner.write('') spinner.text = click.style( - 'Deployment deleted successfully.', fg=Colors.GREEN) - spinner.green.ok(Symbols.SUCCESS) + 'Deployment(s) deleted {}.'.format(text), fg) + spinner.ok(click.style(icon, fg)) except Exception as e: spinner.text = click.style( - 'Failed to delete deployment: {}'.format(e), Colors.RED) + 'Failed to delete deployment(s): {}'.format(e), Colors.RED) spinner.red.fail(Symbols.ERROR) - raise SystemExit(1) + raise SystemExit(1) from e + + +def _apply_delete(result: Queue, deployment: Deployment) -> None: + try: + deployment.deprovision() + result.put((deployment.name, True)) + except Exception: + result.put((deployment.name, False)) diff --git a/riocli/deployment/update.py b/riocli/deployment/update.py index e16da2bf..35bc1175 100644 --- a/riocli/deployment/update.py +++ b/riocli/deployment/update.py @@ -13,19 +13,19 @@ # limitations under the License. import functools -import re from concurrent.futures import ThreadPoolExecutor from queue import Queue -from typing import List import click from click_help_colors import HelpColorsCommand -from rapyuta_io import Client, DeploymentPhaseConstants +from rapyuta_io import Client from rapyuta_io.clients.deployment import Deployment from yaspin.api import Yaspin from riocli.config import new_client from riocli.constants import Symbols, Colors +from riocli.deployment.util import fetch_deployments +from riocli.deployment.util import print_deployments_for_confirmation from riocli.utils import tabulate_data from riocli.utils.spinner import with_spinner @@ -75,11 +75,8 @@ def update_deployment( spinner.ok(Symbols.SUCCESS) return - headers = ['Name', 'GUID', 'Phase', 'Status'] - data = [[d.name, d.deploymentId, d.phase, d.status] for d in deployments] - with spinner.hidden(): - tabulate_data(data, headers) + print_deployments_for_confirmation(deployments) spinner.write('') @@ -125,24 +122,6 @@ def update_deployment( raise SystemExit(1) from e -def fetch_deployments( - client: Client, - deployment_name_or_regex: str, - update_all: bool, -) -> List[Deployment]: - deployments = client.get_all_deployments( - phases=[DeploymentPhaseConstants.SUCCEEDED, - DeploymentPhaseConstants.PROVISIONING]) - result = [] - for deployment in deployments: - if (update_all or deployment.name == deployment_name_or_regex or - (deployment_name_or_regex not in deployment.name and - re.search(deployment_name_or_regex, deployment.name))): - result.append(deployment) - - return result - - def get_component_context(component_info) -> dict: result = {} diff --git a/riocli/deployment/util.py b/riocli/deployment/util.py index 282a84f7..acffb28a 100644 --- a/riocli/deployment/util.py +++ b/riocli/deployment/util.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,17 +13,21 @@ # limitations under the License. import copy import functools +import re import typing +from typing import List import click from rapyuta_io import Client, DeploymentPhaseConstants from rapyuta_io.clients import Device +from rapyuta_io.clients.deployment import Deployment from rapyuta_io.clients.package import ExecutableMount from rapyuta_io.utils import InvalidParameterException, OperationNotAllowedError from rapyuta_io.utils.constants import DEVICE_ID from riocli.config import new_client from riocli.constants import Colors +from riocli.utils import tabulate_data from riocli.utils.selector import show_selection @@ -155,3 +159,28 @@ def add_mount_volume_provision_config(provision_config, component_name, device, provision_config.context['diskMountInfo'].append(tmp_info) return provision_config + + +def fetch_deployments( + client: Client, + deployment_name_or_regex: str, + include_all: bool, +) -> List[Deployment]: + deployments = client.get_all_deployments( + phases=[DeploymentPhaseConstants.SUCCEEDED, + DeploymentPhaseConstants.PROVISIONING]) + result = [] + for deployment in deployments: + if (include_all or + deployment_name_or_regex == deployment.deploymentId or + re.search(deployment_name_or_regex, deployment.name)): + result.append(deployment) + + return result + + +def print_deployments_for_confirmation(deployments: List[Deployment]): + headers = ['Name', 'GUID', 'Phase', 'Status'] + data = [[d.name, d.deploymentId, d.phase, d.status] for d in deployments] + + tabulate_data(data, headers)