clue2app-cli 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clue2app_cli/__init__.py +10 -0
- clue2app_cli/agents/__init__.py +9 -0
- clue2app_cli/agents/ecr_healer.py +475 -0
- clue2app_cli/auth.py +343 -0
- clue2app_cli/config.py +316 -0
- clue2app_cli/core_client.py +46 -0
- clue2app_cli/creds_module.py +253 -0
- clue2app_cli/curses_wizard.py +839 -0
- clue2app_cli/entrypoint_check.py +215 -0
- clue2app_cli/main.py +6711 -0
- clue2app_cli/sdk.py +2993 -0
- clue2app_cli/tui/__init__.py +33 -0
- clue2app_cli/tui/curses_app.py +733 -0
- clue2app_cli/tui/data.py +316 -0
- clue2app_cli/tui/screens/__init__.py +1 -0
- clue2app_cli/tui/textual_app.py +1021 -0
- clue2app_cli/tui/widgets/__init__.py +1 -0
- clue2app_cli/tui_wizard.py +987 -0
- clue2app_cli/version.py +3 -0
- clue2app_cli/wizard.py +1079 -0
- clue2app_cli-0.2.7.dist-info/METADATA +202 -0
- clue2app_cli-0.2.7.dist-info/RECORD +26 -0
- clue2app_cli-0.2.7.dist-info/WHEEL +5 -0
- clue2app_cli-0.2.7.dist-info/entry_points.txt +3 -0
- clue2app_cli-0.2.7.dist-info/licenses/LICENSE +21 -0
- clue2app_cli-0.2.7.dist-info/top_level.txt +1 -0
clue2app_cli/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clue2App CLI - A command line interface for the Clue2App platform.
|
|
3
|
+
|
|
4
|
+
This package provides CLI commands for interacting with the Clue2App platform,
|
|
5
|
+
including authentication, data management, and various app operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__author__ = "Clue2App CLI Team"
|
|
10
|
+
__email__ = "support@clue2app.com"
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ECR Healer Agent - Automatically detects and resolves ECR credential issues.
|
|
3
|
+
|
|
4
|
+
This agent monitors builds for ECR authentication failures and attempts to:
|
|
5
|
+
1. Detect builds that failed due to ECR token expiration
|
|
6
|
+
2. Trigger ECR credential refresh
|
|
7
|
+
3. Retry failed builds
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
# One-time check and heal
|
|
11
|
+
agent = ECRHealerAgent()
|
|
12
|
+
agent.run_once()
|
|
13
|
+
|
|
14
|
+
# Continuous monitoring (runs every 5 minutes)
|
|
15
|
+
agent = ECRHealerAgent(interval=300)
|
|
16
|
+
agent.run_forever()
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import time
|
|
20
|
+
import re
|
|
21
|
+
from typing import Dict, Any, List, Optional, Callable
|
|
22
|
+
from datetime import datetime, timedelta
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
from rich.panel import Panel
|
|
28
|
+
from rich.live import Live
|
|
29
|
+
|
|
30
|
+
from ..sdk import Clue2AppSDK
|
|
31
|
+
from ..config import get_config_value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
console = Console()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class HealingAction:
|
|
39
|
+
"""Represents a healing action taken by the agent."""
|
|
40
|
+
timestamp: datetime
|
|
41
|
+
app_name: str
|
|
42
|
+
project_id: str
|
|
43
|
+
action: str # "refresh_creds", "retry_build", "skip"
|
|
44
|
+
reason: str
|
|
45
|
+
success: bool
|
|
46
|
+
error: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class AgentState:
|
|
51
|
+
"""Tracks the agent's internal state."""
|
|
52
|
+
started_at: datetime = field(default_factory=datetime.now)
|
|
53
|
+
last_check: Optional[datetime] = None
|
|
54
|
+
checks_performed: int = 0
|
|
55
|
+
issues_detected: int = 0
|
|
56
|
+
issues_healed: int = 0
|
|
57
|
+
actions: List[HealingAction] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ECRHealerAgent:
|
|
61
|
+
"""
|
|
62
|
+
Autonomous agent for detecting and healing ECR credential issues.
|
|
63
|
+
|
|
64
|
+
The agent monitors kpack builds for failures related to ECR authentication
|
|
65
|
+
and attempts to automatically resolve them by refreshing credentials and
|
|
66
|
+
retrying builds.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Patterns that indicate ECR credential issues
|
|
70
|
+
ECR_ERROR_PATTERNS = [
|
|
71
|
+
r"authorization token has expired",
|
|
72
|
+
r"DENIED.*reauthenticate",
|
|
73
|
+
r"unable to fetch remote.*ecr",
|
|
74
|
+
r"ecr.*unauthorized",
|
|
75
|
+
r"ecr.*access denied",
|
|
76
|
+
r"no basic auth credentials",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
interval: int = 300, # Check every 5 minutes by default
|
|
82
|
+
max_retries: int = 3,
|
|
83
|
+
dry_run: bool = False,
|
|
84
|
+
on_action: Optional[Callable[[HealingAction], None]] = None,
|
|
85
|
+
verbose: bool = True,
|
|
86
|
+
):
|
|
87
|
+
"""
|
|
88
|
+
Initialize the ECR Healer Agent.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
interval: Seconds between checks in continuous mode
|
|
92
|
+
max_retries: Maximum retry attempts per build
|
|
93
|
+
dry_run: If True, detect issues but don't take action
|
|
94
|
+
on_action: Callback function called when an action is taken
|
|
95
|
+
verbose: If True, print detailed status messages
|
|
96
|
+
"""
|
|
97
|
+
self.sdk = Clue2AppSDK()
|
|
98
|
+
self.interval = interval
|
|
99
|
+
self.max_retries = max_retries
|
|
100
|
+
self.dry_run = dry_run
|
|
101
|
+
self.on_action = on_action
|
|
102
|
+
self.verbose = verbose
|
|
103
|
+
self.state = AgentState()
|
|
104
|
+
self._retry_counts: Dict[str, int] = {} # Track retries per build
|
|
105
|
+
self._stop_requested = False
|
|
106
|
+
|
|
107
|
+
def _log(self, message: str, style: str = ""):
|
|
108
|
+
"""Log a message if verbose mode is enabled."""
|
|
109
|
+
if self.verbose:
|
|
110
|
+
if style:
|
|
111
|
+
console.print(f"[{style}]{message}[/{style}]")
|
|
112
|
+
else:
|
|
113
|
+
console.print(message)
|
|
114
|
+
|
|
115
|
+
def _is_ecr_error(self, message: str) -> bool:
|
|
116
|
+
"""Check if an error message indicates an ECR credential issue."""
|
|
117
|
+
if not message:
|
|
118
|
+
return False
|
|
119
|
+
message_lower = message.lower()
|
|
120
|
+
return any(
|
|
121
|
+
re.search(pattern, message_lower)
|
|
122
|
+
for pattern in self.ECR_ERROR_PATTERNS
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _get_failed_builds(self, project_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
126
|
+
"""Get all builds that have failed due to ECR issues."""
|
|
127
|
+
failed_builds = []
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# Get all images (builds)
|
|
131
|
+
images = self.sdk.list_images(project_id=project_id)
|
|
132
|
+
|
|
133
|
+
for image in images:
|
|
134
|
+
conditions = image.get("status", {}).get("conditions", [])
|
|
135
|
+
if not conditions:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
condition = conditions[0]
|
|
139
|
+
status = condition.get("status")
|
|
140
|
+
reason = condition.get("reason", "")
|
|
141
|
+
message = condition.get("message", "")
|
|
142
|
+
|
|
143
|
+
# Check if it's a failed build with ECR error
|
|
144
|
+
if status == "False" and reason == "BuildFailed":
|
|
145
|
+
if self._is_ecr_error(message):
|
|
146
|
+
failed_builds.append({
|
|
147
|
+
"name": image.get("metadata", {}).get("name"),
|
|
148
|
+
"display_name": image.get("metadata", {}).get("displayName"),
|
|
149
|
+
"uid": image.get("metadata", {}).get("uid"),
|
|
150
|
+
"namespace": image.get("metadata", {}).get("namespace"),
|
|
151
|
+
"message": message,
|
|
152
|
+
"project_id": image.get("metadata", {}).get("labels", {}).get("appGroupId"),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
self._log(f"Error fetching builds: {e}", "red")
|
|
157
|
+
|
|
158
|
+
return failed_builds
|
|
159
|
+
|
|
160
|
+
def _refresh_ecr_credentials(self) -> bool:
|
|
161
|
+
"""
|
|
162
|
+
Trigger ECR credential refresh.
|
|
163
|
+
|
|
164
|
+
This calls the coordinator API to refresh ECR authentication tokens.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
# Call the coordinator to refresh ECR credentials
|
|
168
|
+
response = self.sdk._make_authenticated_request(
|
|
169
|
+
"POST",
|
|
170
|
+
"/api/admin/refresh-ecr-credentials"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if response.status_code == 200:
|
|
174
|
+
self._log("ā ECR credentials refreshed successfully", "green")
|
|
175
|
+
return True
|
|
176
|
+
elif response.status_code == 404:
|
|
177
|
+
# Endpoint might not exist yet, log and continue
|
|
178
|
+
self._log("ā ECR refresh endpoint not available (404)", "yellow")
|
|
179
|
+
return False
|
|
180
|
+
else:
|
|
181
|
+
self._log(f"ā Failed to refresh ECR credentials: {response.status_code}", "red")
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
self._log(f"ā Error refreshing ECR credentials: {e}", "red")
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def _retry_build(self, image_name: str) -> bool:
|
|
189
|
+
"""
|
|
190
|
+
Retry a failed build.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
image_name: The name of the image to rebuild
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
True if rebuild was triggered successfully
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
result = self.sdk.rebuild_app(image_name)
|
|
200
|
+
if result.get("success"):
|
|
201
|
+
self._log(f"ā Triggered rebuild for {image_name}", "green")
|
|
202
|
+
return True
|
|
203
|
+
else:
|
|
204
|
+
self._log(f"ā Failed to trigger rebuild: {result.get('error')}", "red")
|
|
205
|
+
return False
|
|
206
|
+
except Exception as e:
|
|
207
|
+
self._log(f"ā Error triggering rebuild for {image_name}: {e}", "red")
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def _record_action(self, action: HealingAction):
|
|
211
|
+
"""Record an action and call the callback if set."""
|
|
212
|
+
self.state.actions.append(action)
|
|
213
|
+
if self.on_action:
|
|
214
|
+
self.on_action(action)
|
|
215
|
+
|
|
216
|
+
def check_and_heal(self) -> List[HealingAction]:
|
|
217
|
+
"""
|
|
218
|
+
Perform one check cycle - detect ECR issues and attempt to heal them.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of actions taken during this cycle
|
|
222
|
+
"""
|
|
223
|
+
self.state.checks_performed += 1
|
|
224
|
+
self.state.last_check = datetime.now()
|
|
225
|
+
actions = []
|
|
226
|
+
|
|
227
|
+
self._log(f"\n[bold]š Checking for ECR credential issues...[/bold]")
|
|
228
|
+
|
|
229
|
+
# Get all projects
|
|
230
|
+
try:
|
|
231
|
+
projects = self.sdk.list_projects()
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self._log(f"Error fetching projects: {e}", "red")
|
|
234
|
+
return actions
|
|
235
|
+
|
|
236
|
+
all_failed_builds = []
|
|
237
|
+
|
|
238
|
+
# Check each project for failed builds
|
|
239
|
+
for project in projects:
|
|
240
|
+
project_id = project.get("id")
|
|
241
|
+
project_name = project.get("name")
|
|
242
|
+
|
|
243
|
+
failed_builds = self._get_failed_builds(project_id)
|
|
244
|
+
for build in failed_builds:
|
|
245
|
+
build["project_name"] = project_name
|
|
246
|
+
all_failed_builds.append(build)
|
|
247
|
+
|
|
248
|
+
if not all_failed_builds:
|
|
249
|
+
self._log("ā No ECR-related build failures detected", "green")
|
|
250
|
+
return actions
|
|
251
|
+
|
|
252
|
+
self.state.issues_detected += len(all_failed_builds)
|
|
253
|
+
self._log(f"ā Found {len(all_failed_builds)} build(s) with ECR credential issues", "yellow")
|
|
254
|
+
|
|
255
|
+
# Display failed builds
|
|
256
|
+
table = Table(title="Failed Builds (ECR Issues)")
|
|
257
|
+
table.add_column("App", style="cyan")
|
|
258
|
+
table.add_column("Project", style="magenta")
|
|
259
|
+
table.add_column("Error", style="red", max_width=50)
|
|
260
|
+
|
|
261
|
+
for build in all_failed_builds:
|
|
262
|
+
table.add_row(
|
|
263
|
+
build.get("display_name", build.get("name")),
|
|
264
|
+
build.get("project_name", "Unknown"),
|
|
265
|
+
build.get("message", "")[:50] + "..."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
console.print(table)
|
|
269
|
+
|
|
270
|
+
if self.dry_run:
|
|
271
|
+
self._log("\n[yellow]Dry run mode - no actions taken[/yellow]")
|
|
272
|
+
for build in all_failed_builds:
|
|
273
|
+
action = HealingAction(
|
|
274
|
+
timestamp=datetime.now(),
|
|
275
|
+
app_name=build.get("name"),
|
|
276
|
+
project_id=build.get("project_id"),
|
|
277
|
+
action="skip",
|
|
278
|
+
reason="Dry run mode",
|
|
279
|
+
success=True,
|
|
280
|
+
)
|
|
281
|
+
actions.append(action)
|
|
282
|
+
self._record_action(action)
|
|
283
|
+
return actions
|
|
284
|
+
|
|
285
|
+
# Attempt to refresh ECR credentials
|
|
286
|
+
self._log("\n[bold]š§ Attempting to refresh ECR credentials...[/bold]")
|
|
287
|
+
creds_refreshed = self._refresh_ecr_credentials()
|
|
288
|
+
|
|
289
|
+
if not creds_refreshed:
|
|
290
|
+
self._log("ā Could not refresh credentials, will still attempt rebuilds", "yellow")
|
|
291
|
+
|
|
292
|
+
# Retry failed builds
|
|
293
|
+
self._log("\n[bold]š Retrying failed builds...[/bold]")
|
|
294
|
+
|
|
295
|
+
for build in all_failed_builds:
|
|
296
|
+
build_name = build.get("name")
|
|
297
|
+
|
|
298
|
+
# Check retry count
|
|
299
|
+
retry_count = self._retry_counts.get(build_name, 0)
|
|
300
|
+
if retry_count >= self.max_retries:
|
|
301
|
+
self._log(f"ā Skipping {build_name} - max retries ({self.max_retries}) reached", "yellow")
|
|
302
|
+
action = HealingAction(
|
|
303
|
+
timestamp=datetime.now(),
|
|
304
|
+
app_name=build_name,
|
|
305
|
+
project_id=build.get("project_id"),
|
|
306
|
+
action="skip",
|
|
307
|
+
reason=f"Max retries ({self.max_retries}) reached",
|
|
308
|
+
success=False,
|
|
309
|
+
)
|
|
310
|
+
actions.append(action)
|
|
311
|
+
self._record_action(action)
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
# Attempt rebuild
|
|
315
|
+
success = self._retry_build(build_name)
|
|
316
|
+
self._retry_counts[build_name] = retry_count + 1
|
|
317
|
+
|
|
318
|
+
action = HealingAction(
|
|
319
|
+
timestamp=datetime.now(),
|
|
320
|
+
app_name=build_name,
|
|
321
|
+
project_id=build.get("project_id"),
|
|
322
|
+
action="retry_build",
|
|
323
|
+
reason="ECR credential expiration",
|
|
324
|
+
success=success,
|
|
325
|
+
error=None if success else "Rebuild failed",
|
|
326
|
+
)
|
|
327
|
+
actions.append(action)
|
|
328
|
+
self._record_action(action)
|
|
329
|
+
|
|
330
|
+
if success:
|
|
331
|
+
self.state.issues_healed += 1
|
|
332
|
+
|
|
333
|
+
return actions
|
|
334
|
+
|
|
335
|
+
def run_once(self) -> List[HealingAction]:
|
|
336
|
+
"""
|
|
337
|
+
Run a single check and heal cycle.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
List of actions taken
|
|
341
|
+
"""
|
|
342
|
+
self._log(Panel.fit(
|
|
343
|
+
"[bold blue]ECR Healer Agent[/bold blue]\n"
|
|
344
|
+
f"Mode: {'Dry Run' if self.dry_run else 'Active'}\n"
|
|
345
|
+
f"Max Retries: {self.max_retries}",
|
|
346
|
+
title="š¤ Agent Started"
|
|
347
|
+
))
|
|
348
|
+
|
|
349
|
+
actions = self.check_and_heal()
|
|
350
|
+
|
|
351
|
+
self._print_summary()
|
|
352
|
+
return actions
|
|
353
|
+
|
|
354
|
+
def run_forever(self):
|
|
355
|
+
"""
|
|
356
|
+
Run the agent continuously, checking at the configured interval.
|
|
357
|
+
|
|
358
|
+
Use Ctrl+C to stop.
|
|
359
|
+
"""
|
|
360
|
+
self._log(Panel.fit(
|
|
361
|
+
"[bold blue]ECR Healer Agent[/bold blue]\n"
|
|
362
|
+
f"Mode: {'Dry Run' if self.dry_run else 'Active'} (Continuous)\n"
|
|
363
|
+
f"Check Interval: {self.interval}s\n"
|
|
364
|
+
f"Max Retries: {self.max_retries}",
|
|
365
|
+
title="š¤ Agent Started"
|
|
366
|
+
))
|
|
367
|
+
|
|
368
|
+
self._stop_requested = False
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
while not self._stop_requested:
|
|
372
|
+
self.check_and_heal()
|
|
373
|
+
|
|
374
|
+
self._log(f"\nš¤ Next check in {self.interval} seconds... (Ctrl+C to stop)")
|
|
375
|
+
|
|
376
|
+
# Sleep in small increments to allow for quick shutdown
|
|
377
|
+
for _ in range(self.interval):
|
|
378
|
+
if self._stop_requested:
|
|
379
|
+
break
|
|
380
|
+
time.sleep(1)
|
|
381
|
+
|
|
382
|
+
except KeyboardInterrupt:
|
|
383
|
+
self._log("\n\n[yellow]Agent stopped by user[/yellow]")
|
|
384
|
+
finally:
|
|
385
|
+
self._print_summary()
|
|
386
|
+
|
|
387
|
+
def stop(self):
|
|
388
|
+
"""Request the agent to stop (for programmatic control)."""
|
|
389
|
+
self._stop_requested = True
|
|
390
|
+
|
|
391
|
+
def _print_summary(self):
|
|
392
|
+
"""Print a summary of the agent's activities."""
|
|
393
|
+
runtime = datetime.now() - self.state.started_at
|
|
394
|
+
|
|
395
|
+
summary = Table(title="Agent Summary", show_header=False)
|
|
396
|
+
summary.add_column("Metric", style="cyan")
|
|
397
|
+
summary.add_column("Value", style="white")
|
|
398
|
+
|
|
399
|
+
summary.add_row("Runtime", str(runtime).split(".")[0])
|
|
400
|
+
summary.add_row("Checks Performed", str(self.state.checks_performed))
|
|
401
|
+
summary.add_row("Issues Detected", str(self.state.issues_detected))
|
|
402
|
+
summary.add_row("Issues Healed", str(self.state.issues_healed))
|
|
403
|
+
summary.add_row("Total Actions", str(len(self.state.actions)))
|
|
404
|
+
|
|
405
|
+
console.print("\n")
|
|
406
|
+
console.print(summary)
|
|
407
|
+
|
|
408
|
+
if self.state.actions:
|
|
409
|
+
actions_table = Table(title="Actions Taken")
|
|
410
|
+
actions_table.add_column("Time", style="dim")
|
|
411
|
+
actions_table.add_column("App", style="cyan")
|
|
412
|
+
actions_table.add_column("Action", style="magenta")
|
|
413
|
+
actions_table.add_column("Result", style="green")
|
|
414
|
+
|
|
415
|
+
for action in self.state.actions[-10:]: # Last 10 actions
|
|
416
|
+
actions_table.add_row(
|
|
417
|
+
action.timestamp.strftime("%H:%M:%S"),
|
|
418
|
+
action.app_name,
|
|
419
|
+
action.action,
|
|
420
|
+
"ā" if action.success else "ā"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
console.print(actions_table)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def main():
|
|
427
|
+
"""CLI entry point for the ECR healer agent."""
|
|
428
|
+
import argparse
|
|
429
|
+
|
|
430
|
+
parser = argparse.ArgumentParser(description="ECR Healer Agent")
|
|
431
|
+
parser.add_argument(
|
|
432
|
+
"--continuous", "-c",
|
|
433
|
+
action="store_true",
|
|
434
|
+
help="Run continuously instead of one-time check"
|
|
435
|
+
)
|
|
436
|
+
parser.add_argument(
|
|
437
|
+
"--interval", "-i",
|
|
438
|
+
type=int,
|
|
439
|
+
default=300,
|
|
440
|
+
help="Check interval in seconds (default: 300)"
|
|
441
|
+
)
|
|
442
|
+
parser.add_argument(
|
|
443
|
+
"--max-retries", "-r",
|
|
444
|
+
type=int,
|
|
445
|
+
default=3,
|
|
446
|
+
help="Maximum retry attempts per build (default: 3)"
|
|
447
|
+
)
|
|
448
|
+
parser.add_argument(
|
|
449
|
+
"--dry-run", "-n",
|
|
450
|
+
action="store_true",
|
|
451
|
+
help="Detect issues but don't take any action"
|
|
452
|
+
)
|
|
453
|
+
parser.add_argument(
|
|
454
|
+
"--quiet", "-q",
|
|
455
|
+
action="store_true",
|
|
456
|
+
help="Suppress detailed output"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
args = parser.parse_args()
|
|
460
|
+
|
|
461
|
+
agent = ECRHealerAgent(
|
|
462
|
+
interval=args.interval,
|
|
463
|
+
max_retries=args.max_retries,
|
|
464
|
+
dry_run=args.dry_run,
|
|
465
|
+
verbose=not args.quiet,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if args.continuous:
|
|
469
|
+
agent.run_forever()
|
|
470
|
+
else:
|
|
471
|
+
agent.run_once()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
if __name__ == "__main__":
|
|
475
|
+
main()
|