Skip to content

Commit 6491d81

Browse files
committed
fix: add missing files
1 parent dd601c0 commit 6491d81

File tree

1 file changed

+195
-0
lines changed

1 file changed

+195
-0
lines changed

src/emd/utils/smart_bootstrap.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import os
2+
import boto3
3+
import typer
4+
import packaging.version
5+
from typing import Optional, Tuple
6+
from rich.console import Console
7+
from rich.panel import Panel
8+
9+
from emd.revision import VERSION
10+
from emd.constants import ENV_STACK_NAME, ENV_BUCKET_NAME_PREFIX
11+
from emd.utils.logger_utils import get_logger
12+
13+
logger = get_logger(__name__)
14+
15+
16+
def get_deployed_infrastructure_version(region: str) -> Optional[str]:
17+
"""Get the version of currently deployed infrastructure"""
18+
try:
19+
cfn = boto3.client('cloudformation', region_name=region)
20+
stack_info = cfn.describe_stacks(StackName=ENV_STACK_NAME)['Stacks'][0]
21+
22+
# Get ArtifactVersion parameter from stack
23+
for param in stack_info.get('Parameters', []):
24+
if param['ParameterKey'] == 'ArtifactVersion':
25+
return param['ParameterValue']
26+
except Exception as e:
27+
logger.debug(f"Failed to get deployed infrastructure version: {e}")
28+
return None
29+
30+
31+
def check_infrastructure_completeness(region: str) -> Tuple[bool, str]:
32+
"""
33+
Check if EMD infrastructure is completely set up
34+
Returns: (is_complete, status_message)
35+
"""
36+
try:
37+
cfn = boto3.client('cloudformation', region_name=region)
38+
s3 = boto3.client('s3', region_name=region)
39+
40+
# Check CloudFormation stack
41+
try:
42+
stack_info = cfn.describe_stacks(StackName=ENV_STACK_NAME)['Stacks'][0]
43+
stack_status = stack_info['StackStatus']
44+
45+
if stack_status not in ['CREATE_COMPLETE', 'UPDATE_COMPLETE']:
46+
return False, f"CloudFormation stack status: {stack_status}"
47+
48+
except cfn.exceptions.ClientError as e:
49+
if "does not exist" in str(e):
50+
return False, "CloudFormation stack does not exist"
51+
raise
52+
53+
# Check S3 bucket (get bucket name from stack resources)
54+
try:
55+
from emd.sdk.bootstrap import get_bucket_name
56+
bucket_name = get_bucket_name(
57+
bucket_prefix=ENV_BUCKET_NAME_PREFIX,
58+
region=region
59+
)
60+
s3.head_bucket(Bucket=bucket_name)
61+
except Exception as e:
62+
return False, f"S3 bucket issue: {str(e)}"
63+
64+
return True, "Infrastructure is complete"
65+
66+
except Exception as e:
67+
logger.debug(f"Infrastructure completeness check failed: {e}")
68+
return False, f"Infrastructure check failed: {str(e)}"
69+
70+
71+
class SmartBootstrapManager:
72+
def __init__(self):
73+
self.console = Console()
74+
75+
def check_version_compatibility(self, current_version: str, deployed_version: str, region: str) -> str:
76+
"""
77+
Check version compatibility and infrastructure completeness
78+
Returns: 'auto_bootstrap', 'version_mismatch_warning', 'compatible'
79+
"""
80+
# First check if infrastructure is complete
81+
is_complete, status_msg = check_infrastructure_completeness(region)
82+
if not is_complete:
83+
logger.debug(f"Infrastructure incomplete: {status_msg}")
84+
return 'auto_bootstrap' # Infrastructure missing/incomplete, need bootstrap
85+
86+
if not deployed_version:
87+
return 'auto_bootstrap' # No version info, need bootstrap
88+
89+
try:
90+
current_parsed = packaging.version.parse(current_version)
91+
deployed_parsed = packaging.version.parse(deployed_version)
92+
93+
if current_parsed > deployed_parsed:
94+
return 'auto_bootstrap' # Local newer, auto bootstrap
95+
elif deployed_parsed > current_parsed:
96+
return 'version_mismatch_warning' # Cloud newer, show warning
97+
else:
98+
return 'compatible' # Same version, compatible
99+
except Exception as e:
100+
logger.debug(f"Failed to parse versions: {e}")
101+
return 'auto_bootstrap' # Default to bootstrap if parsing fails
102+
103+
def show_bootstrap_notification(self, current_version: str, deployed_version: str):
104+
"""Show notification about automatic bootstrap"""
105+
self.console.print() # Empty line for spacing
106+
if deployed_version:
107+
self.console.print(f"🔄 [bold green]Updating infrastructure...[/bold green] [dim]{deployed_version}[/dim] → [bold green]{current_version}[/bold green]")
108+
else:
109+
self.console.print(f"🚀 [bold green]Setting up infrastructure...[/bold green] [bold green]{current_version}[/bold green]")
110+
self.console.print() # Empty line for spacing
111+
112+
def show_version_mismatch_warning(self, current_version: str, deployed_version: str):
113+
"""Show warning when cloud version is newer than local version"""
114+
self.console.print() # Empty line for spacing
115+
self.console.print(f"⚠️ [bold yellow]Version mismatch:[/bold yellow] Local [dim]{current_version}[/dim] < Cloud [bold yellow]{deployed_version}[/bold yellow]")
116+
self.console.print(f" [bold]Recommendation:[/bold] pip install --upgrade easy-model-deployer")
117+
self.console.print() # Empty line for spacing
118+
119+
def is_auto_bootstrap_disabled(self) -> bool:
120+
"""Check if auto bootstrap is disabled via environment variable"""
121+
return os.getenv("EMD_DISABLE_AUTO_BOOTSTRAP", "").lower() in ["true", "1", "yes"]
122+
123+
def auto_bootstrap_if_needed(self, region: str) -> bool:
124+
"""
125+
Automatically run bootstrap if needed based on comprehensive infrastructure check
126+
Returns: True if bootstrap was run, False otherwise
127+
"""
128+
# Check if auto bootstrap is disabled
129+
if self.is_auto_bootstrap_disabled():
130+
logger.debug("Auto bootstrap disabled via EMD_DISABLE_AUTO_BOOTSTRAP")
131+
return False
132+
133+
current_version = VERSION
134+
deployed_version = get_deployed_infrastructure_version(region)
135+
136+
action = self.check_version_compatibility(current_version, deployed_version, region)
137+
138+
if action == 'compatible':
139+
return False # No action needed
140+
141+
elif action == 'version_mismatch_warning':
142+
# Cloud version > Local version - show warning and ask user
143+
self.show_version_mismatch_warning(current_version, deployed_version)
144+
145+
if not typer.confirm("Continue deployment despite version mismatch?", default=False):
146+
self.console.print("[yellow]Deployment cancelled. Please update EMD to the latest version.[/yellow]")
147+
raise typer.Exit(0)
148+
149+
return False # User chose to continue, no bootstrap
150+
151+
elif action == 'auto_bootstrap':
152+
# Infrastructure missing/incomplete OR version mismatch - ask for confirmation
153+
self.show_bootstrap_notification(current_version, deployed_version)
154+
155+
# Ask for user confirmation
156+
if deployed_version:
157+
# Update scenario
158+
confirm_msg = f"Update infrastructure from {deployed_version} to {current_version}?"
159+
else:
160+
# Initialize scenario
161+
confirm_msg = f"Initialize EMD infrastructure for version {current_version}?"
162+
163+
if not typer.confirm(confirm_msg, default=True):
164+
self.console.print("[yellow]Bootstrap cancelled. Infrastructure will not be updated.[/yellow]")
165+
self.console.print("[red]Deployment cannot proceed without compatible infrastructure.[/red]")
166+
raise typer.Exit(1)
167+
168+
# User confirmed - proceed with bootstrap
169+
try:
170+
from emd.sdk.bootstrap import create_env_stack, get_bucket_name
171+
172+
bucket_name = get_bucket_name(
173+
bucket_prefix=ENV_BUCKET_NAME_PREFIX,
174+
region=region
175+
)
176+
177+
create_env_stack(
178+
region=region,
179+
stack_name=ENV_STACK_NAME,
180+
bucket_name=bucket_name,
181+
force_update=True
182+
)
183+
184+
self.console.print("[bold green]✅ Infrastructure setup completed successfully![/bold green]")
185+
return True
186+
187+
except Exception as e:
188+
self.console.print(f"[bold red]❌ Infrastructure setup failed: {str(e)}[/bold red]")
189+
raise typer.Exit(1)
190+
191+
return False
192+
193+
194+
# Global instance
195+
smart_bootstrap_manager = SmartBootstrapManager()

0 commit comments

Comments
 (0)