Skip to content

Commit eb55296

Browse files
[AKS] az aks command invoke: Add progress spinner (#30274)
1 parent e8bcbfb commit eb55296

File tree

3 files changed

+104
-4
lines changed

3 files changed

+104
-4
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
from typing import Dict
7+
8+
from azure.core.polling.base_polling import LocationPolling, _is_empty, BadResponse, _as_json
9+
10+
11+
class RunCommandLocationPolling(LocationPolling):
12+
"""Extends LocationPolling but uses the body content instead of the status code for the status"""
13+
14+
@staticmethod
15+
def _get_provisioning_state(response):
16+
"""Attempt to get provisioning state from resource.
17+
18+
:param azure.core.pipeline.transport.HttpResponse response: latest REST call response.
19+
:returns: Status if found, else 'None'.
20+
"""
21+
if _is_empty(response):
22+
return None
23+
body: Dict = _as_json(response)
24+
return body.get("properties", {}).get("provisioningState")
25+
26+
def get_status(self, pipeline_response):
27+
"""Process the latest status update retrieved from the same URL as
28+
the previous request.
29+
30+
:param azure.core.pipeline.PipelineResponse response: latest REST call response.
31+
:raises: BadResponse if status not 200 or 204.
32+
"""
33+
response = pipeline_response.http_response
34+
if _is_empty(response):
35+
raise BadResponse(
36+
"The response from long running operation does not contain a body."
37+
)
38+
39+
status = self._get_provisioning_state(response)
40+
return status or "Unknown"

src/azure-cli/azure/cli/command_modules/acs/custom.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
CONST_AZURE_SERVICE_MESH_MODE_ISTIO,
6969
CONST_MANAGED_CLUSTER_SKU_TIER_PREMIUM,
7070
)
71-
71+
from azure.cli.command_modules.acs._polling import RunCommandLocationPolling
7272
from azure.cli.command_modules.acs._helpers import get_snapshot_by_snapshot_id, check_is_private_link_cluster
7373
from azure.cli.command_modules.acs._resourcegroup import get_rg_location
7474
from azure.cli.command_modules.acs._validators import extract_comma_separated_string
@@ -95,9 +95,11 @@
9595
from azure.cli.core.commands import LongRunningOperation
9696
from azure.cli.core.commands.client_factory import get_subscription_id
9797
from azure.cli.core.profiles import ResourceType
98+
from azure.mgmt.core.polling.arm_polling import ARMPolling
9899
from azure.cli.core.util import in_cloud_console, sdk_no_wait
99100
from azure.core.exceptions import ResourceNotFoundError as ResourceNotFoundErrorAzCore
100101
from azure.mgmt.containerservice.models import KubernetesSupportPlan
102+
from humanfriendly.terminal.spinners import Spinner
101103
from knack.log import get_logger
102104
from knack.prompting import NoTTYException, prompt_y_n
103105
from knack.util import CLIError
@@ -2034,6 +2036,7 @@ def aks_runcommand(cmd, client, resource_group_name, name, command_string="", co
20342036

20352037
if not command_string:
20362038
raise ValidationError('Command cannot be empty.')
2039+
20372040
RunCommandRequest = cmd.get_models('RunCommandRequest', resource_type=ResourceType.MGMT_CONTAINERSERVICE,
20382041
operation_group='managed_clusters')
20392042
request_payload = RunCommandRequest(command=command_string)
@@ -2046,8 +2049,15 @@ def aks_runcommand(cmd, client, resource_group_name, name, command_string="", co
20462049
request_payload.cluster_token = _get_dataplane_aad_token(
20472050
cmd.cli_ctx, "6dae42f8-4368-4678-94ff-3960e28e3630")
20482051

2052+
polling_interval = 5
2053+
retry_total = 0
2054+
20492055
command_result_poller = sdk_no_wait(
2050-
no_wait, client.begin_run_command, resource_group_name, name, request_payload, polling_interval=5, retry_total=0
2056+
no_wait, client.begin_run_command, resource_group_name, name, request_payload,
2057+
# NOTE: Note sure if retry_total is used in ARMPolling
2058+
polling=ARMPolling(polling_interval, lro_options={"final-state-via": "location"}, lro_algorithms=[RunCommandLocationPolling()], retry_total=retry_total),
2059+
polling_interval=polling_interval,
2060+
retry_total=retry_total
20512061
)
20522062
if no_wait:
20532063
# pylint: disable=protected-access
@@ -2058,7 +2068,22 @@ def aks_runcommand(cmd, client, resource_group_name, name, command_string="", co
20582068
command_id = command_id_regex.findall(command_result_polling_url)[0]
20592069
_aks_command_result_in_progess_helper(client, resource_group_name, name, command_id)
20602070
return
2061-
return _print_command_result(cmd.cli_ctx, command_result_poller.result(300))
2071+
2072+
spinner = Spinner(label='Running', stream=sys.stderr, hide_cursor=False)
2073+
progress_controller = cmd.cli_ctx.get_progress_controller(det=False, spinner=spinner)
2074+
2075+
now = datetime.datetime.now()
2076+
progress_controller.begin()
2077+
while not command_result_poller.done():
2078+
if datetime.datetime.now() - now >= datetime.timedelta(seconds=300):
2079+
break
2080+
2081+
progress_controller.add(message=command_result_poller.status())
2082+
progress_controller.update()
2083+
time.sleep(0.5)
2084+
2085+
progress_controller.end()
2086+
return _print_command_result(cmd.cli_ctx, command_result_poller.result(timeout=0))
20622087

20632088

20642089
def aks_command_result(cmd, client, resource_group_name, name, command_id=""):
@@ -2115,7 +2140,7 @@ def _print_command_result(cli_ctx, commandResult):
21152140
return
21162141

21172142
# *-ing state
2118-
print(f"{colorama.Fore.BLUE}command is in {commandResult.provisioning_state} state{colorama.Style.RESET_ALL}")
2143+
print(f"{colorama.Fore.BLUE}command (id: {commandResult.id}) is in {commandResult.provisioning_state} state{colorama.Style.RESET_ALL}")
21192144
return
21202145

21212146

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import unittest
7+
from unittest.mock import Mock
8+
9+
from azure.cli.command_modules.acs._polling import RunCommandLocationPolling
10+
from azure.core.pipeline import PipelineResponse
11+
from azure.core.rest import HttpRequest, HttpResponse
12+
13+
14+
class TestRunCommandPoller(unittest.TestCase):
15+
def test_get_status(self):
16+
poller = RunCommandLocationPolling()
17+
18+
mock_response = Mock(spec=HttpResponse)
19+
mock_response.text.return_value = "{\"properties\": {\"provisioningState\": \"Scaling Up\"}}"
20+
21+
pipeline_response: PipelineResponse[HttpRequest, HttpResponse] = PipelineResponse(Mock(spec=HttpRequest), mock_response, Mock())
22+
23+
status = poller.get_status(pipeline_response)
24+
assert status == "Scaling Up"
25+
26+
def test_get_status_no_provisioning_state(self):
27+
poller = RunCommandLocationPolling()
28+
29+
mock_response = Mock(spec=HttpResponse)
30+
mock_response.text.return_value = "{\"properties\": {\"status\": \"Scaling Up\"}}"
31+
32+
pipeline_response: PipelineResponse[HttpRequest, HttpResponse] = PipelineResponse(Mock(spec=HttpRequest), mock_response, Mock())
33+
34+
status = poller.get_status(pipeline_response)
35+
assert status == "Unknown"

0 commit comments

Comments
 (0)