Skip to content

Commit d0bbb3c

Browse files
committed
feat(ecs): add mode parameter and validation logic
Add --mode parameter to enable text-only monitoring and extract validation. Modified: monitorexpressgatewayservice.py (+59), monitormutatinggatewayservice.py - Add --mode parameter (interactive, text-only) - Add validation methods for mode and resources - Auto-detect mode based on TTY availability - Support both interactive and text-only strategies Benefits: CI/CD compatibility, better error messages
1 parent 21b2611 commit d0bbb3c

File tree

2 files changed

+195
-129
lines changed

2 files changed

+195
-129
lines changed

awscli/customizations/ecs/monitorexpressgatewayservice.py

Lines changed: 98 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
allowing users to track resource creation progress, deployment status, and service health through
1919
an interactive command-line interface with live updates and visual indicators.
2020
21-
The data collection logic is handled by ServiceViewCollector, which parses AWS resources and
22-
formats monitoring output. This module focuses on display and user interaction.
23-
2421
The module implements two resource view modes:
2522
- RESOURCE: Displays all resources associated with the service
2623
- DEPLOYMENT: Shows resources that have changed in the most recent deployment
2724
25+
And two display modes:
26+
- INTERACTIVE: Real-time display with spinner and keyboard navigation (requires TTY)
27+
- TEXT-ONLY: Text output with timestamps and change detection (works without TTY)
28+
2829
Key Features:
2930
- Real-time progress monitoring with spinner animations
3031
- Diff-based resource tracking for deployment changes
@@ -36,18 +37,21 @@
3637
ECSExpressGatewayServiceWatcher: Core monitoring logic and resource tracking
3738
3839
Usage:
39-
aws ecs monitor-express-gateway-service --service-arn <arn> [--resource-view RESOURCE|DEPLOYMENT]
40+
aws ecs monitor-express-gateway-service --service-arn <arn> [--resource-view RESOURCE|DEPLOYMENT] [--mode INTERACTIVE|TEXT-ONLY]
4041
"""
4142

42-
import asyncio
4343
import sys
44-
import threading
4544
import time
4645

4746
from botocore.exceptions import ClientError
4847

4948
from awscli.customizations.commands import BasicCommand
5049
from awscli.customizations.ecs.exceptions import MonitoringError
50+
from awscli.customizations.ecs.expressgateway.display_strategy import (
51+
DisplayStrategy,
52+
InteractiveDisplayStrategy,
53+
TextOnlyDisplayStrategy,
54+
)
5155
from awscli.customizations.ecs.prompt_toolkit_display import Display
5256
from awscli.customizations.ecs.serviceviewcollector import ServiceViewCollector
5357
from awscli.customizations.utils import uni_print
@@ -64,14 +68,16 @@ class ECSMonitorExpressGatewayService(BasicCommand):
6468

6569
DESCRIPTION = (
6670
"Monitors the progress of resource creation for an ECS Express Gateway Service. "
67-
"This command provides real-time monitoring of service deployments with interactive "
68-
"progress display, showing the status of load balancers, security groups, auto-scaling "
71+
"This command provides real-time monitoring of service deployments showing the status "
72+
"of load balancers, security groups, auto-scaling "
6973
"configurations, and other AWS resources as they are created or updated. "
7074
"Use ``--resource-view RESOURCE`` to view all service resources, or ``--resource-view DEPLOYMENT`` to track only "
7175
"resources that have changed in the most recent deployment. "
72-
"The command requires a terminal (TTY) to run and the monitoring session continues "
73-
"until manually stopped by the user or the specified timeout is reached. "
74-
"Use keyboard shortcuts to navigate: up/down to scroll through resources, 'q' to quit monitoring."
76+
"Choose ``--mode INTERACTIVE`` for real-time display with keyboard navigation (requires TTY), "
77+
"or ``--mode text-only`` for text output with timestamps (works without TTY). "
78+
"The monitoring session continues until manually stopped by the user or the specified timeout is reached. "
79+
"In interactive mode, use keyboard shortcuts: up/down to scroll through resources, 'q' to quit. "
80+
"In TEXT-ONLY mode, press Ctrl+C to stop monitoring."
7581
)
7682

7783
ARG_TABLE = [
@@ -96,6 +102,16 @@ class ECSMonitorExpressGatewayService(BasicCommand):
96102
'default': 'RESOURCE',
97103
'choices': ['RESOURCE', 'DEPLOYMENT'],
98104
},
105+
{
106+
'name': 'mode',
107+
'help_text': (
108+
"Display mode for monitoring output. "
109+
"interactive (default if TTY available) - Real-time display with spinner and keyboard navigation. "
110+
"text-only - Text output with timestamps and change detection (works without TTY)."
111+
),
112+
'required': False,
113+
'choices': ['interactive', 'text-only'],
114+
},
99115
{
100116
'name': 'timeout',
101117
'help_text': (
@@ -126,15 +142,12 @@ def _run_main(self, parsed_args, parsed_globals):
126142
parsed_globals: Global CLI configuration including region and endpoint
127143
"""
128144
try:
129-
# Check if running in a TTY for interactive display
130-
if not sys.stdout.isatty():
131-
uni_print(
132-
"Error: This command requires a TTY. "
133-
"Please run this command in a terminal.",
134-
sys.stderr,
135-
)
136-
return 1
145+
display_mode = self._determine_display_mode(parsed_args.mode)
146+
except ValueError as e:
147+
uni_print(str(e), sys.stderr)
148+
return 1
137149

150+
try:
138151
self._client = self._session.create_client(
139152
'ecs',
140153
region_name=parsed_globals.region,
@@ -149,14 +162,52 @@ def _run_main(self, parsed_args, parsed_globals):
149162
self._client,
150163
parsed_args.service_arn,
151164
parsed_args.resource_view,
165+
display_mode,
152166
timeout_minutes=parsed_args.timeout,
153167
use_color=use_color,
154168
).exec()
155169
except MonitoringError as e:
156170
uni_print(f"Error monitoring service: {e}", sys.stderr)
171+
return 1
172+
173+
def _determine_display_mode(self, requested_mode):
174+
"""Determine and validate the display mode.
175+
176+
Args:
177+
requested_mode: User-requested mode ('interactive', 'text-only', or None)
178+
179+
Returns:
180+
str: Validated display mode ('interactive' or 'text-only')
181+
182+
Raises:
183+
ValueError: If interactive mode is requested without TTY
184+
"""
185+
# Determine display mode with auto-detection
186+
if requested_mode is None:
187+
# Auto-detect: interactive if TTY available, else text-only
188+
return 'interactive' if sys.stdout.isatty() else 'text-only'
189+
190+
# Validate requested mode
191+
if requested_mode == 'interactive':
192+
if not sys.stdout.isatty():
193+
raise ValueError(
194+
"Error: Interactive mode requires a TTY (terminal). "
195+
"Use --mode text-only for non-interactive environments."
196+
)
197+
return 'interactive'
198+
199+
# text-only mode doesn't require TTY
200+
return requested_mode
157201

158202
def _should_use_color(self, parsed_globals):
159-
"""Determine if color output should be used based on global settings."""
203+
"""Determine if color output should be used based on global settings.
204+
205+
Args:
206+
parsed_globals: Global CLI configuration
207+
208+
Returns:
209+
bool: True if color should be used
210+
"""
160211
if parsed_globals.color == 'on':
161212
return True
162213
elif parsed_globals.color == 'off':
@@ -175,104 +226,51 @@ class ECSExpressGatewayServiceWatcher:
175226
Args:
176227
client: ECS client for API calls
177228
service_arn (str): ARN of the service to monitor
178-
mode (str): Monitoring mode - 'RESOURCE' or 'DEPLOYMENT'
229+
resource_view (str): Resource view mode - 'RESOURCE' or 'DEPLOYMENT'
230+
display_mode (str): Display mode - 'INTERACTIVE' or 'TEXT-ONLY'
179231
timeout_minutes (int): Maximum monitoring time in minutes (default: 30)
180232
"""
181233

182234
def __init__(
183235
self,
184236
client,
185237
service_arn,
186-
mode,
238+
resource_view,
239+
display_mode,
187240
timeout_minutes=30,
188241
display=None,
189242
use_color=True,
190243
collector=None,
191244
):
192245
self._client = client
193246
self.service_arn = service_arn
194-
self.mode = mode
195-
self.timeout_minutes = timeout_minutes
247+
self.display_mode = display_mode
196248
self.start_time = time.time()
197-
self.use_color = use_color
198-
self.display = display or Display()
249+
self.timeout_minutes = timeout_minutes
199250
self.collector = collector or ServiceViewCollector(
200-
client, service_arn, mode, use_color
251+
client, service_arn, resource_view, use_color
201252
)
202-
203-
@staticmethod
204-
def is_monitoring_available():
205-
"""Check if monitoring is available (requires TTY)."""
206-
return sys.stdout.isatty()
253+
self.display = display or Display()
207254

208255
def exec(self):
209-
"""Start monitoring the express gateway service with progress display."""
210-
211-
def monitor_service(spinner_char):
212-
return self.collector.get_current_view(spinner_char)
213-
214-
asyncio.run(self._execute_with_progress_async(monitor_service, 100))
215-
216-
async def _execute_with_progress_async(
217-
self, execution, progress_refresh_millis, execution_refresh_millis=5000
218-
):
219-
"""Execute monitoring loop with animated progress display."""
220-
spinner_chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
221-
spinner_index = 0
222-
223-
current_output = "Waiting for initial data"
224-
225-
async def update_data():
226-
nonlocal current_output
227-
while True:
228-
current_time = time.time()
229-
if current_time - self.start_time > self.timeout_minutes * 60:
230-
break
231-
try:
232-
loop = asyncio.get_event_loop()
233-
new_output = await loop.run_in_executor(
234-
None, execution, "{SPINNER}"
235-
)
236-
current_output = new_output
237-
except ClientError as e:
238-
if (
239-
e.response.get('Error', {}).get('Code')
240-
== 'InvalidParameterException'
241-
):
242-
error_message = e.response.get('Error', {}).get(
243-
'Message', ''
244-
)
245-
if (
246-
"Cannot call DescribeServiceRevisions for a service that is INACTIVE"
247-
in error_message
248-
):
249-
current_output = "Service is inactive"
250-
else:
251-
raise
252-
else:
253-
raise
254-
await asyncio.sleep(execution_refresh_millis / 1000.0)
255-
256-
async def update_spinner():
257-
nonlocal spinner_index
258-
while True:
259-
spinner_char = spinner_chars[spinner_index]
260-
display_output = current_output.replace(
261-
"{SPINNER}", spinner_char
262-
)
263-
status_text = f"Getting updates... {spinner_char} | up/down to scroll, q to quit"
264-
self.display.display(display_output, status_text)
265-
spinner_index = (spinner_index + 1) % len(spinner_chars)
266-
await asyncio.sleep(progress_refresh_millis / 1000.0)
256+
"""Execute monitoring using the appropriate display strategy."""
257+
strategy = self._create_display_strategy()
258+
strategy.execute(
259+
collector=self.collector,
260+
start_time=self.start_time,
261+
timeout_minutes=self.timeout_minutes,
262+
)
267263

268-
# Start both tasks
269-
data_task = asyncio.create_task(update_data())
270-
spinner_task = asyncio.create_task(update_spinner())
264+
def _create_display_strategy(self):
265+
"""Create display strategy based on display mode.
271266
272-
try:
273-
await self.display.run()
274-
finally:
275-
data_task.cancel()
276-
spinner_task.cancel()
277-
final_output = current_output.replace("{SPINNER}", "")
278-
uni_print(final_output + "\nMonitoring Complete!\n")
267+
Returns:
268+
DisplayStrategy: Appropriate strategy for the selected mode
269+
"""
270+
if self.display_mode == 'text-only':
271+
return TextOnlyDisplayStrategy(use_color=self.collector.use_color)
272+
else:
273+
return InteractiveDisplayStrategy(
274+
display=self.display,
275+
use_color=self.collector.use_color,
276+
)

0 commit comments

Comments
 (0)