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.
@@ -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,9 @@
1
+ """
2
+ Agents module for Clue2App CLI.
3
+
4
+ Provides autonomous agents for monitoring and self-healing operations.
5
+ """
6
+
7
+ from .ecr_healer import ECRHealerAgent
8
+
9
+ __all__ = ["ECRHealerAgent"]
@@ -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()