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/wizard.py
ADDED
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Interactive Deploy Wizard with animations for Clue2App CLI.
|
|
4
|
+
|
|
5
|
+
This module provides a step-by-step wizard experience similar to the console UI,
|
|
6
|
+
with animated transitions and rich visual feedback.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Optional, List, Dict, Any, Callable
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
import questionary
|
|
15
|
+
from questionary import Style
|
|
16
|
+
from rich.console import Console, Group
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
from rich.live import Live
|
|
21
|
+
from rich.layout import Layout
|
|
22
|
+
from rich.align import Align
|
|
23
|
+
from rich.box import ROUNDED, DOUBLE, HEAVY
|
|
24
|
+
from rich.spinner import Spinner
|
|
25
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
# Custom questionary style matching Clue2App theme
|
|
30
|
+
WIZARD_STYLE = Style([
|
|
31
|
+
('qmark', 'fg:#3b82f6 bold'), # Blue question mark
|
|
32
|
+
('question', 'bold'), # Bold question
|
|
33
|
+
('answer', 'fg:#22c55e bold'), # Green answer
|
|
34
|
+
('pointer', 'fg:#3b82f6 bold'), # Blue pointer
|
|
35
|
+
('highlighted', 'fg:#3b82f6 bold'), # Blue highlighted
|
|
36
|
+
('selected', 'fg:#22c55e'), # Green selected
|
|
37
|
+
('separator', 'fg:#64748b'), # Gray separator
|
|
38
|
+
('instruction', 'fg:#94a3b8'), # Light gray instructions
|
|
39
|
+
('text', ''), # Default text
|
|
40
|
+
('disabled', 'fg:#64748b italic'), # Disabled option
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class WizardStep:
|
|
46
|
+
"""Represents a single step in the wizard."""
|
|
47
|
+
name: str
|
|
48
|
+
title: str
|
|
49
|
+
description: str
|
|
50
|
+
icon: str = "○"
|
|
51
|
+
fields: List[Dict[str, Any]] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class WizardState:
|
|
56
|
+
"""Holds the current state of the wizard."""
|
|
57
|
+
current_step: int = 0
|
|
58
|
+
total_steps: int = 5
|
|
59
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
completed_steps: List[int] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AnimatedStepIndicator:
|
|
64
|
+
"""Renders an animated step indicator like the UI wizard."""
|
|
65
|
+
|
|
66
|
+
STEP_ICONS = {
|
|
67
|
+
'pending': '○',
|
|
68
|
+
'active': '●',
|
|
69
|
+
'completed': '✓',
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
COLORS = {
|
|
73
|
+
'pending': 'dim white',
|
|
74
|
+
'active': 'bold blue',
|
|
75
|
+
'completed': 'bold green',
|
|
76
|
+
'connector_done': 'green',
|
|
77
|
+
'connector_pending': 'dim white',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def __init__(self, steps: List[WizardStep], current: int = 0, completed: List[int] = None):
|
|
81
|
+
self.steps = steps
|
|
82
|
+
self.current = current
|
|
83
|
+
self.completed = completed or []
|
|
84
|
+
|
|
85
|
+
def render(self) -> Panel:
|
|
86
|
+
"""Render the step indicator as a Rich Panel."""
|
|
87
|
+
parts = []
|
|
88
|
+
|
|
89
|
+
for i, step in enumerate(self.steps):
|
|
90
|
+
# Determine step state
|
|
91
|
+
if i in self.completed:
|
|
92
|
+
state = 'completed'
|
|
93
|
+
icon = self.STEP_ICONS['completed']
|
|
94
|
+
color = self.COLORS['completed']
|
|
95
|
+
elif i == self.current:
|
|
96
|
+
state = 'active'
|
|
97
|
+
icon = self.STEP_ICONS['active']
|
|
98
|
+
color = self.COLORS['active']
|
|
99
|
+
else:
|
|
100
|
+
state = 'pending'
|
|
101
|
+
icon = self.STEP_ICONS['pending']
|
|
102
|
+
color = self.COLORS['pending']
|
|
103
|
+
|
|
104
|
+
# Build step text
|
|
105
|
+
step_text = Text()
|
|
106
|
+
step_text.append(f" {icon} ", style=color)
|
|
107
|
+
step_text.append(f"{step.name}", style=color if state != 'pending' else 'dim')
|
|
108
|
+
parts.append(step_text)
|
|
109
|
+
|
|
110
|
+
# Add connector (except for last step)
|
|
111
|
+
if i < len(self.steps) - 1:
|
|
112
|
+
connector_color = self.COLORS['connector_done'] if i in self.completed else self.COLORS['connector_pending']
|
|
113
|
+
connector = Text(" ─── ", style=connector_color)
|
|
114
|
+
parts.append(connector)
|
|
115
|
+
|
|
116
|
+
# Combine all parts
|
|
117
|
+
combined = Text()
|
|
118
|
+
for part in parts:
|
|
119
|
+
combined.append(part)
|
|
120
|
+
|
|
121
|
+
return Panel(
|
|
122
|
+
Align.center(combined),
|
|
123
|
+
border_style="blue",
|
|
124
|
+
box=ROUNDED,
|
|
125
|
+
padding=(0, 1),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class DeployWizard:
|
|
130
|
+
"""Interactive deployment wizard with animations."""
|
|
131
|
+
|
|
132
|
+
STEPS = [
|
|
133
|
+
WizardStep(
|
|
134
|
+
name="Basics",
|
|
135
|
+
title="Application Basics",
|
|
136
|
+
description="Give your application a name and select target cluster",
|
|
137
|
+
icon="📱",
|
|
138
|
+
),
|
|
139
|
+
WizardStep(
|
|
140
|
+
name="Source",
|
|
141
|
+
title="Source Code",
|
|
142
|
+
description="Configure your Git repository",
|
|
143
|
+
icon="📦",
|
|
144
|
+
),
|
|
145
|
+
WizardStep(
|
|
146
|
+
name="Registry",
|
|
147
|
+
title="Container Registry",
|
|
148
|
+
description="Choose where to store your container images",
|
|
149
|
+
icon="🐳",
|
|
150
|
+
),
|
|
151
|
+
WizardStep(
|
|
152
|
+
name="Settings",
|
|
153
|
+
title="Application Settings",
|
|
154
|
+
description="Configure deployment, scaling, and environment",
|
|
155
|
+
icon="⚙️",
|
|
156
|
+
),
|
|
157
|
+
WizardStep(
|
|
158
|
+
name="Review",
|
|
159
|
+
title="Review & Deploy",
|
|
160
|
+
description="Review your configuration and deploy",
|
|
161
|
+
icon="🚀",
|
|
162
|
+
),
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
def __init__(self, projects: List[Dict] = None):
|
|
166
|
+
self.state = WizardState(total_steps=len(self.STEPS))
|
|
167
|
+
self.projects = projects or []
|
|
168
|
+
self.indicator = AnimatedStepIndicator(self.STEPS)
|
|
169
|
+
|
|
170
|
+
def clear_screen(self):
|
|
171
|
+
"""Clear the terminal screen."""
|
|
172
|
+
console.clear()
|
|
173
|
+
|
|
174
|
+
def show_header(self):
|
|
175
|
+
"""Display the wizard header with step indicator."""
|
|
176
|
+
self.indicator.current = self.state.current_step
|
|
177
|
+
self.indicator.completed = self.state.completed_steps
|
|
178
|
+
|
|
179
|
+
# Title
|
|
180
|
+
title = Text()
|
|
181
|
+
title.append("🚀 ", style="bold")
|
|
182
|
+
title.append("Deploy Application Wizard", style="bold blue")
|
|
183
|
+
|
|
184
|
+
console.print()
|
|
185
|
+
console.print(Align.center(title))
|
|
186
|
+
console.print()
|
|
187
|
+
console.print(self.indicator.render())
|
|
188
|
+
console.print()
|
|
189
|
+
|
|
190
|
+
def animate_transition(self, direction: str = "forward"):
|
|
191
|
+
"""Animate the transition between steps."""
|
|
192
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
193
|
+
|
|
194
|
+
with Live(console=console, refresh_per_second=20) as live:
|
|
195
|
+
for i, frame in enumerate(frames):
|
|
196
|
+
text = Text()
|
|
197
|
+
text.append(f" {frame} ", style="blue")
|
|
198
|
+
if direction == "forward":
|
|
199
|
+
text.append("Loading next step...", style="dim")
|
|
200
|
+
else:
|
|
201
|
+
text.append("Going back...", style="dim")
|
|
202
|
+
live.update(Align.center(text))
|
|
203
|
+
time.sleep(0.03)
|
|
204
|
+
|
|
205
|
+
def step_basics(self) -> bool:
|
|
206
|
+
"""Step 1: Application basics."""
|
|
207
|
+
self.clear_screen()
|
|
208
|
+
self.show_header()
|
|
209
|
+
|
|
210
|
+
step = self.STEPS[0]
|
|
211
|
+
console.print(Panel(
|
|
212
|
+
f"[bold]{step.icon} {step.title}[/bold]\n[dim]{step.description}[/dim]",
|
|
213
|
+
border_style="blue",
|
|
214
|
+
box=ROUNDED,
|
|
215
|
+
))
|
|
216
|
+
console.print()
|
|
217
|
+
|
|
218
|
+
# Application name
|
|
219
|
+
app_name = questionary.text(
|
|
220
|
+
"Application Name:",
|
|
221
|
+
instruction="(lowercase, alphanumeric, hyphens allowed)",
|
|
222
|
+
validate=lambda x: len(x) >= 2 or "Name must be at least 2 characters",
|
|
223
|
+
style=WIZARD_STYLE,
|
|
224
|
+
).ask()
|
|
225
|
+
|
|
226
|
+
if app_name is None: # User cancelled
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
self.state.data['app_name'] = app_name.lower().replace(' ', '-').replace('_', '-')
|
|
230
|
+
|
|
231
|
+
# Project selection (if multiple projects available)
|
|
232
|
+
if self.projects:
|
|
233
|
+
project_choices = [
|
|
234
|
+
questionary.Choice(
|
|
235
|
+
title=f"{p.get('name')} ({p.get('ns', 'default')})",
|
|
236
|
+
value=p
|
|
237
|
+
) for p in self.projects
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
selected_project = questionary.select(
|
|
241
|
+
"Select Project:",
|
|
242
|
+
choices=project_choices,
|
|
243
|
+
style=WIZARD_STYLE,
|
|
244
|
+
).ask()
|
|
245
|
+
|
|
246
|
+
if selected_project is None:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
self.state.data['project'] = selected_project
|
|
250
|
+
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
def step_source(self) -> bool:
|
|
254
|
+
"""Step 2: Source code configuration."""
|
|
255
|
+
self.clear_screen()
|
|
256
|
+
self.show_header()
|
|
257
|
+
|
|
258
|
+
step = self.STEPS[1]
|
|
259
|
+
console.print(Panel(
|
|
260
|
+
f"[bold]{step.icon} {step.title}[/bold]\n[dim]{step.description}[/dim]",
|
|
261
|
+
border_style="blue",
|
|
262
|
+
box=ROUNDED,
|
|
263
|
+
))
|
|
264
|
+
console.print()
|
|
265
|
+
|
|
266
|
+
# Git URL
|
|
267
|
+
git_url = questionary.text(
|
|
268
|
+
"Git Repository URL:",
|
|
269
|
+
instruction="(e.g., https://github.com/user/repo.git)",
|
|
270
|
+
validate=lambda x: x.startswith(('http://', 'https://', 'git@')) or "Enter a valid Git URL",
|
|
271
|
+
style=WIZARD_STYLE,
|
|
272
|
+
).ask()
|
|
273
|
+
|
|
274
|
+
if git_url is None:
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
self.state.data['git_url'] = git_url
|
|
278
|
+
|
|
279
|
+
# Branch
|
|
280
|
+
branch = questionary.select(
|
|
281
|
+
"Git Branch:",
|
|
282
|
+
choices=[
|
|
283
|
+
questionary.Choice("main", value="main"),
|
|
284
|
+
questionary.Choice("master", value="master"),
|
|
285
|
+
questionary.Choice("Custom...", value="custom"),
|
|
286
|
+
],
|
|
287
|
+
style=WIZARD_STYLE,
|
|
288
|
+
).ask()
|
|
289
|
+
|
|
290
|
+
if branch is None:
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
if branch == "custom":
|
|
294
|
+
branch = questionary.text(
|
|
295
|
+
"Enter branch name:",
|
|
296
|
+
style=WIZARD_STYLE,
|
|
297
|
+
).ask()
|
|
298
|
+
if branch is None:
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
self.state.data['branch'] = branch
|
|
302
|
+
|
|
303
|
+
# Private repo?
|
|
304
|
+
is_private = questionary.confirm(
|
|
305
|
+
"Is this a private repository?",
|
|
306
|
+
default=False,
|
|
307
|
+
style=WIZARD_STYLE,
|
|
308
|
+
).ask()
|
|
309
|
+
|
|
310
|
+
if is_private is None:
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
self.state.data['is_private'] = is_private
|
|
314
|
+
|
|
315
|
+
if is_private:
|
|
316
|
+
git_token = questionary.password(
|
|
317
|
+
"Git Token/Password:",
|
|
318
|
+
style=WIZARD_STYLE,
|
|
319
|
+
).ask()
|
|
320
|
+
|
|
321
|
+
if git_token is None:
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
self.state.data['git_token'] = git_token
|
|
325
|
+
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
def step_registry(self) -> bool:
|
|
329
|
+
"""Step 3: Container registry configuration."""
|
|
330
|
+
self.clear_screen()
|
|
331
|
+
self.show_header()
|
|
332
|
+
|
|
333
|
+
step = self.STEPS[2]
|
|
334
|
+
console.print(Panel(
|
|
335
|
+
f"[bold]{step.icon} {step.title}[/bold]\n[dim]{step.description}[/dim]",
|
|
336
|
+
border_style="blue",
|
|
337
|
+
box=ROUNDED,
|
|
338
|
+
))
|
|
339
|
+
console.print()
|
|
340
|
+
|
|
341
|
+
registry_type = questionary.select(
|
|
342
|
+
"Container Registry:",
|
|
343
|
+
choices=[
|
|
344
|
+
questionary.Choice(
|
|
345
|
+
"Platform Registry (Recommended)",
|
|
346
|
+
value="PLATFORM_REGISTRY",
|
|
347
|
+
),
|
|
348
|
+
questionary.Choice(
|
|
349
|
+
"Private Registry",
|
|
350
|
+
value="PRIVATE_REGISTRY",
|
|
351
|
+
),
|
|
352
|
+
],
|
|
353
|
+
style=WIZARD_STYLE,
|
|
354
|
+
).ask()
|
|
355
|
+
|
|
356
|
+
if registry_type is None:
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
self.state.data['registry_type'] = registry_type
|
|
360
|
+
|
|
361
|
+
if registry_type == "PRIVATE_REGISTRY":
|
|
362
|
+
registry_url = questionary.text(
|
|
363
|
+
"Registry URL:",
|
|
364
|
+
instruction="(e.g., docker.io, gcr.io, your-registry.com)",
|
|
365
|
+
style=WIZARD_STYLE,
|
|
366
|
+
).ask()
|
|
367
|
+
|
|
368
|
+
if registry_url is None:
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
self.state.data['registry_url'] = registry_url
|
|
372
|
+
|
|
373
|
+
registry_user = questionary.text(
|
|
374
|
+
"Registry Username:",
|
|
375
|
+
style=WIZARD_STYLE,
|
|
376
|
+
).ask()
|
|
377
|
+
|
|
378
|
+
registry_pass = questionary.password(
|
|
379
|
+
"Registry Password:",
|
|
380
|
+
style=WIZARD_STYLE,
|
|
381
|
+
).ask()
|
|
382
|
+
|
|
383
|
+
if registry_user is None or registry_pass is None:
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
self.state.data['registry_user'] = registry_user
|
|
387
|
+
self.state.data['registry_pass'] = registry_pass
|
|
388
|
+
|
|
389
|
+
return True
|
|
390
|
+
|
|
391
|
+
def step_settings(self) -> bool:
|
|
392
|
+
"""Step 4: Application settings."""
|
|
393
|
+
self.clear_screen()
|
|
394
|
+
self.show_header()
|
|
395
|
+
|
|
396
|
+
step = self.STEPS[3]
|
|
397
|
+
console.print(Panel(
|
|
398
|
+
f"[bold]{step.icon} {step.title}[/bold]\n[dim]{step.description}[/dim]",
|
|
399
|
+
border_style="blue",
|
|
400
|
+
box=ROUNDED,
|
|
401
|
+
))
|
|
402
|
+
console.print()
|
|
403
|
+
|
|
404
|
+
# Container port
|
|
405
|
+
port = questionary.text(
|
|
406
|
+
"Container Port:",
|
|
407
|
+
default="8080",
|
|
408
|
+
validate=lambda x: x.isdigit() and 1 <= int(x) <= 65535 or "Enter a valid port (1-65535)",
|
|
409
|
+
style=WIZARD_STYLE,
|
|
410
|
+
).ask()
|
|
411
|
+
|
|
412
|
+
if port is None:
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
self.state.data['port'] = int(port)
|
|
416
|
+
|
|
417
|
+
# Application size
|
|
418
|
+
size = questionary.select(
|
|
419
|
+
"Application Size:",
|
|
420
|
+
choices=[
|
|
421
|
+
questionary.Choice("Tiny (256MB RAM, 0.25 CPU)", value="TINY"),
|
|
422
|
+
questionary.Choice("Small (512MB RAM, 0.5 CPU)", value="SMALL"),
|
|
423
|
+
questionary.Choice("Medium (1GB RAM, 1 CPU)", value="MEDIUM"),
|
|
424
|
+
questionary.Choice("Large (2GB RAM, 2 CPU)", value="LARGE"),
|
|
425
|
+
],
|
|
426
|
+
default="SMALL",
|
|
427
|
+
style=WIZARD_STYLE,
|
|
428
|
+
).ask()
|
|
429
|
+
|
|
430
|
+
if size is None:
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
self.state.data['size'] = size
|
|
434
|
+
|
|
435
|
+
# Deploy automatically?
|
|
436
|
+
auto_deploy = questionary.confirm(
|
|
437
|
+
"Deploy automatically after build?",
|
|
438
|
+
default=True,
|
|
439
|
+
style=WIZARD_STYLE,
|
|
440
|
+
).ask()
|
|
441
|
+
|
|
442
|
+
if auto_deploy is None:
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
self.state.data['auto_deploy'] = auto_deploy
|
|
446
|
+
|
|
447
|
+
# Scale to zero?
|
|
448
|
+
scale_to_zero = questionary.confirm(
|
|
449
|
+
"Enable scale to zero? (saves resources when idle)",
|
|
450
|
+
default=False,
|
|
451
|
+
style=WIZARD_STYLE,
|
|
452
|
+
).ask()
|
|
453
|
+
|
|
454
|
+
if scale_to_zero is None:
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
self.state.data['scale_to_zero'] = scale_to_zero
|
|
458
|
+
|
|
459
|
+
# Environment variables
|
|
460
|
+
add_env = questionary.confirm(
|
|
461
|
+
"Add environment variables?",
|
|
462
|
+
default=False,
|
|
463
|
+
style=WIZARD_STYLE,
|
|
464
|
+
).ask()
|
|
465
|
+
|
|
466
|
+
if add_env:
|
|
467
|
+
env_vars = {}
|
|
468
|
+
while True:
|
|
469
|
+
env_input = questionary.text(
|
|
470
|
+
"Environment variable (KEY=VALUE, or empty to finish):",
|
|
471
|
+
style=WIZARD_STYLE,
|
|
472
|
+
).ask()
|
|
473
|
+
|
|
474
|
+
if not env_input:
|
|
475
|
+
break
|
|
476
|
+
|
|
477
|
+
if '=' in env_input:
|
|
478
|
+
key, value = env_input.split('=', 1)
|
|
479
|
+
env_vars[key.strip()] = value.strip()
|
|
480
|
+
console.print(f" [green]✓[/green] Added: {key.strip()}")
|
|
481
|
+
|
|
482
|
+
self.state.data['env_vars'] = env_vars
|
|
483
|
+
|
|
484
|
+
return True
|
|
485
|
+
|
|
486
|
+
def step_review(self) -> bool:
|
|
487
|
+
"""Step 5: Review and deploy."""
|
|
488
|
+
self.clear_screen()
|
|
489
|
+
self.show_header()
|
|
490
|
+
|
|
491
|
+
step = self.STEPS[4]
|
|
492
|
+
console.print(Panel(
|
|
493
|
+
f"[bold]{step.icon} {step.title}[/bold]\n[dim]{step.description}[/dim]",
|
|
494
|
+
border_style="blue",
|
|
495
|
+
box=ROUNDED,
|
|
496
|
+
))
|
|
497
|
+
console.print()
|
|
498
|
+
|
|
499
|
+
# Build review table
|
|
500
|
+
table = Table(
|
|
501
|
+
show_header=False,
|
|
502
|
+
box=ROUNDED,
|
|
503
|
+
border_style="blue",
|
|
504
|
+
padding=(0, 2),
|
|
505
|
+
expand=True,
|
|
506
|
+
)
|
|
507
|
+
table.add_column("Property", style="cyan", width=20)
|
|
508
|
+
table.add_column("Value", style="white")
|
|
509
|
+
|
|
510
|
+
# Basic Info section
|
|
511
|
+
table.add_row("[bold]BASIC INFO[/bold]", "")
|
|
512
|
+
table.add_row("Application Name", self.state.data.get('app_name', 'N/A'))
|
|
513
|
+
if 'project' in self.state.data:
|
|
514
|
+
project = self.state.data['project']
|
|
515
|
+
table.add_row("Project", f"{project.get('name')} ({project.get('ns')})")
|
|
516
|
+
|
|
517
|
+
# Source Code section
|
|
518
|
+
table.add_row("", "")
|
|
519
|
+
table.add_row("[bold]SOURCE CODE[/bold]", "")
|
|
520
|
+
table.add_row("Git Repository", self.state.data.get('git_url', 'N/A'))
|
|
521
|
+
table.add_row("Branch", self.state.data.get('branch', 'main'))
|
|
522
|
+
table.add_row("Access", "[yellow]Private[/yellow]" if self.state.data.get('is_private') else "[green]Public[/green]")
|
|
523
|
+
|
|
524
|
+
# Registry section
|
|
525
|
+
table.add_row("", "")
|
|
526
|
+
table.add_row("[bold]CONTAINER REGISTRY[/bold]", "")
|
|
527
|
+
registry = self.state.data.get('registry_type', 'PLATFORM_REGISTRY')
|
|
528
|
+
if registry == 'PLATFORM_REGISTRY':
|
|
529
|
+
table.add_row("Registry", "[green]Platform Registry[/green]")
|
|
530
|
+
else:
|
|
531
|
+
table.add_row("Registry", self.state.data.get('registry_url', 'N/A'))
|
|
532
|
+
|
|
533
|
+
# Settings section
|
|
534
|
+
table.add_row("", "")
|
|
535
|
+
table.add_row("[bold]SETTINGS[/bold]", "")
|
|
536
|
+
table.add_row("Container Port", str(self.state.data.get('port', 8080)))
|
|
537
|
+
table.add_row("Application Size", self.state.data.get('size', 'SMALL'))
|
|
538
|
+
table.add_row("Auto Deploy", "[green]Yes[/green]" if self.state.data.get('auto_deploy', True) else "[yellow]No[/yellow]")
|
|
539
|
+
table.add_row("Scale to Zero", "[green]Yes[/green]" if self.state.data.get('scale_to_zero') else "[dim]No[/dim]")
|
|
540
|
+
|
|
541
|
+
# Environment variables
|
|
542
|
+
env_vars = self.state.data.get('env_vars', {})
|
|
543
|
+
if env_vars:
|
|
544
|
+
table.add_row("", "")
|
|
545
|
+
table.add_row("[bold]ENVIRONMENT[/bold]", "")
|
|
546
|
+
for key, value in env_vars.items():
|
|
547
|
+
# Mask sensitive values
|
|
548
|
+
display_value = value if len(value) <= 20 else value[:17] + "..."
|
|
549
|
+
table.add_row(f" {key}", display_value)
|
|
550
|
+
|
|
551
|
+
console.print(table)
|
|
552
|
+
console.print()
|
|
553
|
+
|
|
554
|
+
# Confirm deployment
|
|
555
|
+
action = questionary.select(
|
|
556
|
+
"What would you like to do?",
|
|
557
|
+
choices=[
|
|
558
|
+
questionary.Choice("🚀 Deploy Application", value="deploy"),
|
|
559
|
+
questionary.Choice("✏️ Edit Configuration", value="edit"),
|
|
560
|
+
questionary.Choice("❌ Cancel", value="cancel"),
|
|
561
|
+
],
|
|
562
|
+
style=WIZARD_STYLE,
|
|
563
|
+
).ask()
|
|
564
|
+
|
|
565
|
+
if action == "deploy":
|
|
566
|
+
self.state.data['confirmed'] = True
|
|
567
|
+
return True
|
|
568
|
+
elif action == "edit":
|
|
569
|
+
# Go back to first step
|
|
570
|
+
self.state.current_step = 0
|
|
571
|
+
self.state.completed_steps = []
|
|
572
|
+
return self.run_wizard()
|
|
573
|
+
else:
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
def run_wizard(self) -> Optional[Dict[str, Any]]:
|
|
577
|
+
"""Run the complete wizard flow."""
|
|
578
|
+
step_functions = [
|
|
579
|
+
self.step_basics,
|
|
580
|
+
self.step_source,
|
|
581
|
+
self.step_registry,
|
|
582
|
+
self.step_settings,
|
|
583
|
+
self.step_review,
|
|
584
|
+
]
|
|
585
|
+
|
|
586
|
+
while self.state.current_step < len(step_functions):
|
|
587
|
+
step_func = step_functions[self.state.current_step]
|
|
588
|
+
|
|
589
|
+
# Run the step
|
|
590
|
+
result = step_func()
|
|
591
|
+
|
|
592
|
+
if result:
|
|
593
|
+
# Mark step as completed and move to next
|
|
594
|
+
if self.state.current_step not in self.state.completed_steps:
|
|
595
|
+
self.state.completed_steps.append(self.state.current_step)
|
|
596
|
+
|
|
597
|
+
self.state.current_step += 1
|
|
598
|
+
|
|
599
|
+
# Animate transition (except after last step)
|
|
600
|
+
if self.state.current_step < len(step_functions):
|
|
601
|
+
self.animate_transition("forward")
|
|
602
|
+
else:
|
|
603
|
+
# User cancelled or went back
|
|
604
|
+
if self.state.current_step > 0:
|
|
605
|
+
# Ask if they want to go back
|
|
606
|
+
go_back = questionary.confirm(
|
|
607
|
+
"Go back to previous step?",
|
|
608
|
+
default=True,
|
|
609
|
+
style=WIZARD_STYLE,
|
|
610
|
+
).ask()
|
|
611
|
+
|
|
612
|
+
if go_back:
|
|
613
|
+
self.state.current_step -= 1
|
|
614
|
+
self.animate_transition("backward")
|
|
615
|
+
else:
|
|
616
|
+
# Cancel wizard
|
|
617
|
+
return None
|
|
618
|
+
else:
|
|
619
|
+
# Cancel wizard from first step
|
|
620
|
+
return None
|
|
621
|
+
|
|
622
|
+
# Wizard completed
|
|
623
|
+
if self.state.data.get('confirmed'):
|
|
624
|
+
return self.state.data
|
|
625
|
+
|
|
626
|
+
return None
|
|
627
|
+
|
|
628
|
+
def show_deployment_progress(self, deploy_func: Callable) -> bool:
|
|
629
|
+
"""Show animated deployment progress."""
|
|
630
|
+
self.clear_screen()
|
|
631
|
+
|
|
632
|
+
console.print()
|
|
633
|
+
console.print(Align.center(Text("🚀 Deploying Application", style="bold blue")))
|
|
634
|
+
console.print()
|
|
635
|
+
|
|
636
|
+
stages = [
|
|
637
|
+
("Validating configuration...", 0.5),
|
|
638
|
+
("Creating application...", 1.0),
|
|
639
|
+
("Setting up Git source...", 0.8),
|
|
640
|
+
("Configuring registry...", 0.5),
|
|
641
|
+
("Initiating build...", 1.5),
|
|
642
|
+
("Finalizing deployment...", 0.5),
|
|
643
|
+
]
|
|
644
|
+
|
|
645
|
+
with Progress(
|
|
646
|
+
SpinnerColumn(),
|
|
647
|
+
TextColumn("[progress.description]{task.description}"),
|
|
648
|
+
console=console,
|
|
649
|
+
) as progress:
|
|
650
|
+
|
|
651
|
+
for stage_text, duration in stages:
|
|
652
|
+
task = progress.add_task(stage_text, total=None)
|
|
653
|
+
time.sleep(duration)
|
|
654
|
+
progress.update(task, description=f"[green]✓[/green] {stage_text}")
|
|
655
|
+
progress.stop_task(task)
|
|
656
|
+
|
|
657
|
+
# Actually deploy
|
|
658
|
+
console.print()
|
|
659
|
+
try:
|
|
660
|
+
result = deploy_func(self.state.data)
|
|
661
|
+
|
|
662
|
+
console.print()
|
|
663
|
+
console.print(Panel(
|
|
664
|
+
"[bold green]✓ Deployment Initiated Successfully![/bold green]\n\n"
|
|
665
|
+
f"[dim]Application:[/dim] [bold]{self.state.data.get('app_name')}[/bold]\n"
|
|
666
|
+
f"[dim]Status:[/dim] Building\n\n"
|
|
667
|
+
"[dim]Next steps:[/dim]\n"
|
|
668
|
+
" • Run [bold]c2a builds list[/bold] to monitor build progress\n"
|
|
669
|
+
" • Run [bold]c2a services list[/bold] to see running services",
|
|
670
|
+
border_style="green",
|
|
671
|
+
box=DOUBLE,
|
|
672
|
+
))
|
|
673
|
+
|
|
674
|
+
return True
|
|
675
|
+
|
|
676
|
+
except Exception as e:
|
|
677
|
+
console.print()
|
|
678
|
+
console.print(Panel(
|
|
679
|
+
f"[bold red]✗ Deployment Failed[/bold red]\n\n"
|
|
680
|
+
f"[red]{str(e)}[/red]",
|
|
681
|
+
border_style="red",
|
|
682
|
+
box=DOUBLE,
|
|
683
|
+
))
|
|
684
|
+
return False
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
class LiveBuildMonitor:
|
|
688
|
+
"""Real-time build progress monitor with animations."""
|
|
689
|
+
|
|
690
|
+
BUILD_PHASES = [
|
|
691
|
+
("PREPARE", "Preparing build environment"),
|
|
692
|
+
("DETECT", "Detecting buildpacks"),
|
|
693
|
+
("ANALYZE", "Analyzing source code"),
|
|
694
|
+
("RESTORE", "Restoring cached layers"),
|
|
695
|
+
("BUILD", "Building application"),
|
|
696
|
+
("EXPORT", "Exporting container image"),
|
|
697
|
+
("CACHE", "Caching layers"),
|
|
698
|
+
("COMPLETION", "Completing build"),
|
|
699
|
+
]
|
|
700
|
+
|
|
701
|
+
PHASE_ICONS = {
|
|
702
|
+
"pending": "○",
|
|
703
|
+
"running": "◉",
|
|
704
|
+
"completed": "✓",
|
|
705
|
+
"failed": "✗",
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
def __init__(self, app_name: str, namespace: str = None):
|
|
709
|
+
self.app_name = app_name
|
|
710
|
+
self.namespace = namespace
|
|
711
|
+
self.current_phase = 0
|
|
712
|
+
self.build_status = "BUILDING"
|
|
713
|
+
self.logs: List[str] = []
|
|
714
|
+
self.start_time = time.time()
|
|
715
|
+
|
|
716
|
+
def render_progress(self) -> Panel:
|
|
717
|
+
"""Render the current build progress."""
|
|
718
|
+
# Header
|
|
719
|
+
elapsed = int(time.time() - self.start_time)
|
|
720
|
+
elapsed_str = f"{elapsed // 60}m {elapsed % 60}s"
|
|
721
|
+
|
|
722
|
+
header = Text()
|
|
723
|
+
header.append("🔨 ", style="bold")
|
|
724
|
+
header.append("Building: ", style="bold blue")
|
|
725
|
+
header.append(self.app_name, style="bold cyan")
|
|
726
|
+
header.append(f" [dim]({elapsed_str})[/dim]")
|
|
727
|
+
|
|
728
|
+
# Phase progress
|
|
729
|
+
phases_text = Text()
|
|
730
|
+
for i, (phase_name, phase_desc) in enumerate(self.BUILD_PHASES):
|
|
731
|
+
if i < self.current_phase:
|
|
732
|
+
icon = self.PHASE_ICONS["completed"]
|
|
733
|
+
style = "green"
|
|
734
|
+
elif i == self.current_phase:
|
|
735
|
+
icon = self.PHASE_ICONS["running"]
|
|
736
|
+
style = "blue bold"
|
|
737
|
+
else:
|
|
738
|
+
icon = self.PHASE_ICONS["pending"]
|
|
739
|
+
style = "dim"
|
|
740
|
+
|
|
741
|
+
phases_text.append(f"\n {icon} ", style=style)
|
|
742
|
+
phases_text.append(f"{phase_name}: ", style=style)
|
|
743
|
+
phases_text.append(phase_desc, style="dim" if style == "dim" else "")
|
|
744
|
+
|
|
745
|
+
# Recent logs
|
|
746
|
+
logs_text = Text()
|
|
747
|
+
if self.logs:
|
|
748
|
+
logs_text.append("\n\n[dim]Recent logs:[/dim]\n")
|
|
749
|
+
for log in self.logs[-5:]: # Show last 5 log lines
|
|
750
|
+
logs_text.append(f" [dim]{log[:80]}[/dim]\n")
|
|
751
|
+
|
|
752
|
+
# Status bar
|
|
753
|
+
status_text = Text()
|
|
754
|
+
if self.build_status == "BUILDING":
|
|
755
|
+
status_text.append("\n[blue]● Building...[/blue]")
|
|
756
|
+
elif self.build_status == "SUCCESS":
|
|
757
|
+
status_text.append("\n[green]✓ Build completed successfully![/green]")
|
|
758
|
+
elif self.build_status == "FAILED":
|
|
759
|
+
status_text.append("\n[red]✗ Build failed[/red]")
|
|
760
|
+
|
|
761
|
+
content = Group(header, phases_text, logs_text, status_text)
|
|
762
|
+
|
|
763
|
+
return Panel(
|
|
764
|
+
content,
|
|
765
|
+
border_style="blue" if self.build_status == "BUILDING" else ("green" if self.build_status == "SUCCESS" else "red"),
|
|
766
|
+
box=ROUNDED,
|
|
767
|
+
title="[bold]Build Progress[/bold]",
|
|
768
|
+
subtitle="[dim]Press Ctrl+C to run in background[/dim]",
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
def update_from_status(self, status: Dict[str, Any]):
|
|
772
|
+
"""Update monitor state from API status response."""
|
|
773
|
+
conditions = status.get('conditions', [])
|
|
774
|
+
|
|
775
|
+
for cond in conditions:
|
|
776
|
+
if cond.get('type') == 'Ready':
|
|
777
|
+
if cond.get('status') == 'True':
|
|
778
|
+
self.build_status = "SUCCESS"
|
|
779
|
+
self.current_phase = len(self.BUILD_PHASES)
|
|
780
|
+
elif cond.get('reason') == 'BuildFailed':
|
|
781
|
+
self.build_status = "FAILED"
|
|
782
|
+
break
|
|
783
|
+
|
|
784
|
+
# Try to determine phase from latest build reason
|
|
785
|
+
reason = status.get('latestBuildReason', '')
|
|
786
|
+
if 'CONFIG' in reason or 'TRIGGER' in reason:
|
|
787
|
+
self.current_phase = min(1, self.current_phase)
|
|
788
|
+
elif 'STACK' in reason:
|
|
789
|
+
self.current_phase = min(2, self.current_phase)
|
|
790
|
+
elif 'BUILDPACK' in reason:
|
|
791
|
+
self.current_phase = min(4, self.current_phase)
|
|
792
|
+
|
|
793
|
+
def add_log(self, log_line: str):
|
|
794
|
+
"""Add a log line to the display."""
|
|
795
|
+
self.logs.append(log_line)
|
|
796
|
+
if len(self.logs) > 100: # Keep last 100 lines
|
|
797
|
+
self.logs = self.logs[-100:]
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
class LiveServiceMonitor:
|
|
801
|
+
"""Real-time service deployment monitor."""
|
|
802
|
+
|
|
803
|
+
SERVICE_STATES = [
|
|
804
|
+
("PENDING", "Service pending"),
|
|
805
|
+
("CREATING", "Creating pods"),
|
|
806
|
+
("CONFIGURING", "Configuring networking"),
|
|
807
|
+
("SCALING", "Scaling replicas"),
|
|
808
|
+
("READY", "Service ready"),
|
|
809
|
+
]
|
|
810
|
+
|
|
811
|
+
def __init__(self, service_name: str, namespace: str = None):
|
|
812
|
+
self.service_name = service_name
|
|
813
|
+
self.namespace = namespace
|
|
814
|
+
self.current_state = 0
|
|
815
|
+
self.status = "DEPLOYING"
|
|
816
|
+
self.url: Optional[str] = None
|
|
817
|
+
self.pods: int = 0
|
|
818
|
+
self.ready_pods: int = 0
|
|
819
|
+
self.start_time = time.time()
|
|
820
|
+
|
|
821
|
+
def render_progress(self) -> Panel:
|
|
822
|
+
"""Render the current service deployment progress."""
|
|
823
|
+
elapsed = int(time.time() - self.start_time)
|
|
824
|
+
elapsed_str = f"{elapsed // 60}m {elapsed % 60}s"
|
|
825
|
+
|
|
826
|
+
# Header
|
|
827
|
+
header = Text()
|
|
828
|
+
header.append("🚀 ", style="bold")
|
|
829
|
+
header.append("Deploying: ", style="bold blue")
|
|
830
|
+
header.append(self.service_name, style="bold cyan")
|
|
831
|
+
header.append(f" [dim]({elapsed_str})[/dim]")
|
|
832
|
+
|
|
833
|
+
# State progress
|
|
834
|
+
states_text = Text()
|
|
835
|
+
for i, (state_name, state_desc) in enumerate(self.SERVICE_STATES):
|
|
836
|
+
if i < self.current_state:
|
|
837
|
+
icon = "✓"
|
|
838
|
+
style = "green"
|
|
839
|
+
elif i == self.current_state:
|
|
840
|
+
icon = "◉"
|
|
841
|
+
style = "blue bold"
|
|
842
|
+
else:
|
|
843
|
+
icon = "○"
|
|
844
|
+
style = "dim"
|
|
845
|
+
|
|
846
|
+
states_text.append(f"\n {icon} ", style=style)
|
|
847
|
+
states_text.append(f"{state_name}: ", style=style)
|
|
848
|
+
states_text.append(state_desc, style="dim" if style == "dim" else "")
|
|
849
|
+
|
|
850
|
+
# Pod status
|
|
851
|
+
pod_text = Text()
|
|
852
|
+
pod_text.append(f"\n\n Pods: ")
|
|
853
|
+
if self.pods > 0:
|
|
854
|
+
pod_text.append(f"[green]{self.ready_pods}[/green]/{self.pods} ready")
|
|
855
|
+
else:
|
|
856
|
+
pod_text.append("[dim]waiting...[/dim]")
|
|
857
|
+
|
|
858
|
+
# URL
|
|
859
|
+
url_text = Text()
|
|
860
|
+
if self.url:
|
|
861
|
+
url_text.append(f"\n URL: [blue]{self.url}[/blue]")
|
|
862
|
+
|
|
863
|
+
# Status
|
|
864
|
+
status_text = Text()
|
|
865
|
+
if self.status == "DEPLOYING":
|
|
866
|
+
status_text.append("\n\n[blue]● Deploying...[/blue]")
|
|
867
|
+
elif self.status == "READY":
|
|
868
|
+
status_text.append("\n\n[green]✓ Service is ready![/green]")
|
|
869
|
+
elif self.status == "FAILED":
|
|
870
|
+
status_text.append("\n\n[red]✗ Deployment failed[/red]")
|
|
871
|
+
|
|
872
|
+
content = Group(header, states_text, pod_text, url_text, status_text)
|
|
873
|
+
|
|
874
|
+
border_color = "blue" if self.status == "DEPLOYING" else ("green" if self.status == "READY" else "red")
|
|
875
|
+
|
|
876
|
+
return Panel(
|
|
877
|
+
content,
|
|
878
|
+
border_style=border_color,
|
|
879
|
+
box=ROUNDED,
|
|
880
|
+
title="[bold]Service Deployment[/bold]",
|
|
881
|
+
subtitle="[dim]Press Ctrl+C to run in background[/dim]",
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
def update_from_status(self, status: Dict[str, Any]):
|
|
885
|
+
"""Update from Knative service status."""
|
|
886
|
+
conditions = status.get('conditions', [])
|
|
887
|
+
|
|
888
|
+
# Check Ready condition
|
|
889
|
+
for cond in conditions:
|
|
890
|
+
if cond.get('type') == 'Ready':
|
|
891
|
+
if cond.get('status') == 'True':
|
|
892
|
+
self.status = "READY"
|
|
893
|
+
self.current_state = len(self.SERVICE_STATES)
|
|
894
|
+
elif cond.get('status') == 'False':
|
|
895
|
+
reason = cond.get('reason', '')
|
|
896
|
+
if 'Failed' in reason or 'Error' in reason:
|
|
897
|
+
self.status = "FAILED"
|
|
898
|
+
break
|
|
899
|
+
|
|
900
|
+
# Get URL
|
|
901
|
+
self.url = status.get('url')
|
|
902
|
+
|
|
903
|
+
# Estimate state from conditions
|
|
904
|
+
ready_count = sum(1 for c in conditions if c.get('status') == 'True')
|
|
905
|
+
self.current_state = min(ready_count, len(self.SERVICE_STATES) - 1)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def monitor_build_live(
|
|
909
|
+
sdk_client,
|
|
910
|
+
image_name: str,
|
|
911
|
+
namespace: str,
|
|
912
|
+
timeout: int = 600,
|
|
913
|
+
poll_interval: float = 2.0
|
|
914
|
+
) -> bool:
|
|
915
|
+
"""
|
|
916
|
+
Monitor a build in real-time with live updates.
|
|
917
|
+
|
|
918
|
+
Args:
|
|
919
|
+
sdk_client: The SDK client for API calls
|
|
920
|
+
image_name: Name of the image being built
|
|
921
|
+
namespace: Kubernetes namespace
|
|
922
|
+
timeout: Maximum time to wait (seconds)
|
|
923
|
+
poll_interval: Time between status checks
|
|
924
|
+
|
|
925
|
+
Returns:
|
|
926
|
+
True if build succeeded, False otherwise
|
|
927
|
+
"""
|
|
928
|
+
monitor = LiveBuildMonitor(image_name, namespace)
|
|
929
|
+
start_time = time.time()
|
|
930
|
+
|
|
931
|
+
with Live(monitor.render_progress(), console=console, refresh_per_second=4) as live:
|
|
932
|
+
while time.time() - start_time < timeout:
|
|
933
|
+
try:
|
|
934
|
+
# Get image status
|
|
935
|
+
images = sdk_client.apps.get_images(namespace=namespace)
|
|
936
|
+
image = next((img for img in images if image_name in img.get('metadata', {}).get('name', '')), None)
|
|
937
|
+
|
|
938
|
+
if image:
|
|
939
|
+
monitor.update_from_status(image.get('status', {}))
|
|
940
|
+
|
|
941
|
+
# Simulate phase progress based on time for demo
|
|
942
|
+
elapsed = time.time() - start_time
|
|
943
|
+
if monitor.build_status == "BUILDING":
|
|
944
|
+
monitor.current_phase = min(int(elapsed / 10), len(monitor.BUILD_PHASES) - 1)
|
|
945
|
+
|
|
946
|
+
live.update(monitor.render_progress())
|
|
947
|
+
|
|
948
|
+
if monitor.build_status in ("SUCCESS", "FAILED"):
|
|
949
|
+
time.sleep(1) # Brief pause to show final state
|
|
950
|
+
break
|
|
951
|
+
|
|
952
|
+
time.sleep(poll_interval)
|
|
953
|
+
|
|
954
|
+
except KeyboardInterrupt:
|
|
955
|
+
console.print("\n[yellow]Continuing in background...[/yellow]")
|
|
956
|
+
console.print(f" Run [bold]c2a builds logs {image_name}[/bold] to view logs")
|
|
957
|
+
return True
|
|
958
|
+
|
|
959
|
+
except Exception as e:
|
|
960
|
+
monitor.add_log(f"Error: {str(e)}")
|
|
961
|
+
live.update(monitor.render_progress())
|
|
962
|
+
time.sleep(poll_interval)
|
|
963
|
+
|
|
964
|
+
return monitor.build_status == "SUCCESS"
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def monitor_service_live(
|
|
968
|
+
sdk_client,
|
|
969
|
+
service_name: str,
|
|
970
|
+
namespace: str,
|
|
971
|
+
timeout: int = 300,
|
|
972
|
+
poll_interval: float = 2.0
|
|
973
|
+
) -> bool:
|
|
974
|
+
"""
|
|
975
|
+
Monitor a service deployment in real-time.
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
sdk_client: The SDK client for API calls
|
|
979
|
+
service_name: Name of the service
|
|
980
|
+
namespace: Kubernetes namespace
|
|
981
|
+
timeout: Maximum time to wait
|
|
982
|
+
poll_interval: Time between checks
|
|
983
|
+
|
|
984
|
+
Returns:
|
|
985
|
+
True if deployment succeeded, False otherwise
|
|
986
|
+
"""
|
|
987
|
+
monitor = LiveServiceMonitor(service_name, namespace)
|
|
988
|
+
start_time = time.time()
|
|
989
|
+
|
|
990
|
+
with Live(monitor.render_progress(), console=console, refresh_per_second=4) as live:
|
|
991
|
+
while time.time() - start_time < timeout:
|
|
992
|
+
try:
|
|
993
|
+
# In a real implementation, this would call the service status API
|
|
994
|
+
# For now, simulate progress
|
|
995
|
+
elapsed = time.time() - start_time
|
|
996
|
+
|
|
997
|
+
if elapsed > 5:
|
|
998
|
+
monitor.pods = 1
|
|
999
|
+
if elapsed > 10:
|
|
1000
|
+
monitor.ready_pods = 1
|
|
1001
|
+
if elapsed > 15:
|
|
1002
|
+
monitor.url = f"https://{service_name}.{namespace}.apps.clue2.app"
|
|
1003
|
+
if elapsed > 20:
|
|
1004
|
+
monitor.status = "READY"
|
|
1005
|
+
monitor.current_state = len(monitor.SERVICE_STATES)
|
|
1006
|
+
|
|
1007
|
+
live.update(monitor.render_progress())
|
|
1008
|
+
|
|
1009
|
+
if monitor.status in ("READY", "FAILED"):
|
|
1010
|
+
time.sleep(1)
|
|
1011
|
+
break
|
|
1012
|
+
|
|
1013
|
+
time.sleep(poll_interval)
|
|
1014
|
+
|
|
1015
|
+
except KeyboardInterrupt:
|
|
1016
|
+
console.print("\n[yellow]Continuing in background...[/yellow]")
|
|
1017
|
+
console.print(f" Run [bold]c2a services logs {service_name}[/bold] to view logs")
|
|
1018
|
+
return True
|
|
1019
|
+
|
|
1020
|
+
except Exception as e:
|
|
1021
|
+
live.update(monitor.render_progress())
|
|
1022
|
+
time.sleep(poll_interval)
|
|
1023
|
+
|
|
1024
|
+
return monitor.status == "READY"
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def run_deploy_wizard(projects: List[Dict] = None, deploy_func: Callable = None, sdk_client=None) -> Optional[Dict[str, Any]]:
|
|
1028
|
+
"""
|
|
1029
|
+
Run the interactive deploy wizard.
|
|
1030
|
+
|
|
1031
|
+
Args:
|
|
1032
|
+
projects: List of available projects
|
|
1033
|
+
deploy_func: Function to call for actual deployment
|
|
1034
|
+
sdk_client: SDK client for live monitoring
|
|
1035
|
+
|
|
1036
|
+
Returns:
|
|
1037
|
+
Wizard data if completed, None if cancelled
|
|
1038
|
+
"""
|
|
1039
|
+
wizard = DeployWizard(projects=projects)
|
|
1040
|
+
|
|
1041
|
+
try:
|
|
1042
|
+
result = wizard.run_wizard()
|
|
1043
|
+
|
|
1044
|
+
if result and deploy_func:
|
|
1045
|
+
# Show initial deployment progress
|
|
1046
|
+
success = wizard.show_deployment_progress(deploy_func)
|
|
1047
|
+
|
|
1048
|
+
if success and sdk_client:
|
|
1049
|
+
# Monitor build progress
|
|
1050
|
+
app_name = result.get('app_name')
|
|
1051
|
+
namespace = result.get('project', {}).get('ns', 'default')
|
|
1052
|
+
|
|
1053
|
+
console.print()
|
|
1054
|
+
console.print("[bold]Monitoring build progress...[/bold]")
|
|
1055
|
+
console.print("[dim]Press Ctrl+C to run in background[/dim]")
|
|
1056
|
+
console.print()
|
|
1057
|
+
|
|
1058
|
+
build_success = monitor_build_live(
|
|
1059
|
+
sdk_client,
|
|
1060
|
+
f"{app_name}-image",
|
|
1061
|
+
namespace,
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
if build_success and result.get('auto_deploy', True):
|
|
1065
|
+
console.print()
|
|
1066
|
+
console.print("[bold]Monitoring service deployment...[/bold]")
|
|
1067
|
+
console.print()
|
|
1068
|
+
|
|
1069
|
+
monitor_service_live(
|
|
1070
|
+
sdk_client,
|
|
1071
|
+
app_name,
|
|
1072
|
+
namespace,
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
return result
|
|
1076
|
+
|
|
1077
|
+
except KeyboardInterrupt:
|
|
1078
|
+
console.print("\n[yellow]Wizard cancelled.[/yellow]")
|
|
1079
|
+
return None
|