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