13
13
from e3 .os .process import PIPE
14
14
from e3 .vcs .git import GitRepository
15
15
from e3 .aws import AWSEnv , Session
16
+ from e3 .aws .s3 import bucket
16
17
from e3 .aws .cfn import Stack
17
18
from e3 .env import Env
18
19
from e3 .fs import find , sync_tree
@@ -26,6 +27,7 @@ def __init__(
26
27
self ,
27
28
regions : list [str ],
28
29
default_profile : str | None = None ,
30
+ assets_dir : str | None = None ,
29
31
data_dir : str | None = None ,
30
32
s3_bucket : str | None = None ,
31
33
s3_key : str = "" ,
@@ -37,6 +39,7 @@ def __init__(
37
39
38
40
:param regions: list of regions on which we can operate
39
41
:param default_profile: default AWS profile to use to create the stack
42
+ :param assets_dir: directory containing assets of the stack
40
43
:param data_dir: directory containing files used by cfn-init
41
44
:param s3_bucket: if defined S3 will be used as a proxy for resources.
42
45
Template body will be uploaded to S3 before calling operation on
@@ -140,8 +143,11 @@ def __init__(
140
143
141
144
self .regions = regions
142
145
146
+ self .assets_dir = assets_dir
143
147
self .data_dir = data_dir
144
148
self .s3_bucket = s3_bucket
149
+ self .s3_assets_key = None
150
+ self .s3_assets_url = None
145
151
self .s3_data_key = None
146
152
self .s3_data_url = None
147
153
self .s3_template_key = None
@@ -150,23 +156,28 @@ def __init__(
150
156
self .assume_role = assume_role
151
157
self .aws_env : Session | AWSEnv | None = None
152
158
self .deploy_branch = deploy_branch
159
+ # A temporary dir will be assigned when generating assets
160
+ self .gen_assets_dir : str | None = None
153
161
154
162
self .timestamp = datetime .utcnow ().strftime ("%Y-%m-%d/%H:%M:%S.%f" )
155
163
156
164
if s3_bucket is not None :
157
- s3_root_key = (
158
- "/" .join ([s3_key .rstrip ("/" ), self .timestamp ]).strip ("/" ) + "/"
159
- )
160
- self .s3_data_key = s3_root_key + "data/"
161
- self .s3_data_url = "https://%s.s3.amazonaws.com/%s" % (
162
- self .s3_bucket ,
163
- self .s3_data_key ,
164
- )
165
- self .s3_template_key = s3_root_key + "template"
166
- self .s3_template_url = "https://%s.s3.amazonaws.com/%s" % (
167
- self .s3_bucket ,
168
- self .s3_template_key ,
165
+ s3_root_key = f"{ s3_key .strip ('/' )} /"
166
+ s3_root_url = f"https://{ self .s3_bucket } .s3.amazonaws.com/"
167
+
168
+ # Assets use a static key
169
+ self .s3_assets_key = f"{ s3_root_key } assets/"
170
+ self .s3_assets_url = f"{ s3_root_url } { self .s3_assets_key } "
171
+
172
+ # Data and template use a dynamic key based on the timestamp
173
+ s3_timestamp_key = (
174
+ "/" .join ([s3_root_key .rstrip ("/" ), self .timestamp ]).strip ("/" ) + "/"
169
175
)
176
+ self .s3_data_key = f"{ s3_timestamp_key } data/"
177
+ self .s3_data_url = f"{ s3_root_url } { self .s3_data_key } "
178
+
179
+ self .s3_template_key = f"{ s3_timestamp_key } template"
180
+ self .s3_template_url = f"{ s3_root_url } { self .s3_template_key } "
170
181
171
182
@property
172
183
def dry_run (self ) -> bool :
@@ -200,6 +211,116 @@ def _prompt_yes(self, msg: str) -> bool:
200
211
ask = input (f"{ msg } (y/N): " )
201
212
return ask [0 ] in "Yy"
202
213
214
+ def _upload_dir (
215
+ self ,
216
+ root_dir : str ,
217
+ s3_bucket : str ,
218
+ s3_key : str ,
219
+ s3_client : botocore .client .S3 | None = None ,
220
+ check_exists : bool = False ,
221
+ ) -> None :
222
+ """Upload directory to S3 bucket.
223
+
224
+ :param root_dir: directory
225
+ :param s3_bucket: bucket where to upload files
226
+ :param s3_key: key prefix for uploaded files
227
+ :param s3_client: a client for the S3 API
228
+ :param check_exists: check if an S3 object exists before uploading it
229
+ """
230
+ assert self .args is not None
231
+
232
+ for f in find (root_dir ):
233
+ subkey = os .path .relpath (f , root_dir ).replace ("\\ " , "/" )
234
+
235
+ logging .info (
236
+ "Upload %s to %s:%s%s" ,
237
+ subkey ,
238
+ s3_bucket ,
239
+ s3_key ,
240
+ subkey ,
241
+ )
242
+
243
+ if s3_client is None :
244
+ continue
245
+
246
+ with bucket (
247
+ s3_bucket , client = s3_client , auto_create = False
248
+ ) as upload_bucket :
249
+ # Check already existing S3 objects.
250
+ # Ignore the potential 403 error as CFN roles often only have the
251
+ # s3:GetObject permission on the bucket
252
+ s3_object_key = f"{ s3_key } { subkey } "
253
+ if check_exists and upload_bucket .object_exists (
254
+ s3_object_key , ignore_error_403 = True
255
+ ):
256
+ logging .info (
257
+ "Skip already existing %s" ,
258
+ subkey ,
259
+ )
260
+ continue
261
+
262
+ if not self .args .dry_run :
263
+ with open (f , "rb" ) as fd :
264
+ upload_bucket .push (key = s3_object_key , content = fd , exist_ok = True )
265
+
266
+ def _upload_stack (self , stack : Stack ) -> None :
267
+ """Upload stack assets, data, and template to S3.
268
+
269
+ :param stack: the stack to upload
270
+ """
271
+ # Nothing to upload if there is no S3 bucket set
272
+ if self .s3_bucket is None :
273
+ return
274
+
275
+ assert self .args is not None
276
+
277
+ if self .aws_env :
278
+ s3 = self .aws_env .client ("s3" )
279
+ else :
280
+ s3 = None
281
+ logging .warning (
282
+ "no aws session, won't be able to check if assets exist in the bucket"
283
+ )
284
+
285
+ # Synchronize assets to the bucket before creating the stack
286
+ if self .gen_assets_dir is not None and self .s3_assets_key is not None :
287
+ self ._upload_dir (
288
+ root_dir = self .gen_assets_dir ,
289
+ s3_bucket = self .s3_bucket ,
290
+ s3_key = self .s3_assets_key ,
291
+ s3_client = s3 ,
292
+ check_exists = True ,
293
+ )
294
+
295
+ with tempfile .TemporaryDirectory () as tempd :
296
+ # Push data associated with CFNMain and then all data
297
+ # related to the stack
298
+ self .create_data_dir (root_dir = tempd )
299
+ stack .create_data_dir (root_dir = tempd )
300
+
301
+ if self .s3_data_key is not None :
302
+ # synchronize data to the bucket before creating the stack
303
+ self ._upload_dir (
304
+ root_dir = tempd ,
305
+ s3_bucket = self .s3_bucket ,
306
+ s3_key = self .s3_data_key ,
307
+ s3_client = s3 ,
308
+ )
309
+
310
+ if self .s3_template_key is not None :
311
+ logging .info (
312
+ "Upload template to %s:%s" ,
313
+ self .s3_bucket ,
314
+ self .s3_template_key ,
315
+ )
316
+ if s3 is not None and not self .args .dry_run :
317
+ s3 .put_object (
318
+ Bucket = self .s3_bucket ,
319
+ Body = stack .body .encode ("utf-8" ),
320
+ ServerSideEncryption = "AES256" ,
321
+ Key = self .s3_template_key ,
322
+ )
323
+
203
324
def _push_stack_changeset (self , stack : Stack , s3_template_url : str | None ) -> int :
204
325
"""Push the changeset of a stack from an already uploaded S3 template.
205
326
@@ -280,47 +401,7 @@ def execute_for_stack(self, stack: Stack, aws_env: Session | None = None) -> int
280
401
281
402
if self .args .command in ("push" , "update" ):
282
403
# Synchronize resources to the S3 bucket
283
- if not self .args .dry_run :
284
- assert self .aws_env
285
- s3 = self .aws_env .client ("s3" )
286
-
287
- with tempfile .TemporaryDirectory () as tempd :
288
- # Push data associated with CFNMain and then all data
289
- # related to the stack
290
- self .create_data_dir (root_dir = tempd )
291
- stack .create_data_dir (root_dir = tempd )
292
-
293
- if self .s3_data_key is not None :
294
- # synchronize data to the bucket before creating the stack
295
- for f in find (tempd ):
296
- with open (f , "rb" ) as fd :
297
- subkey = os .path .relpath (f , tempd ).replace ("\\ " , "/" )
298
- logging .info (
299
- "Upload %s to %s:%s%s" ,
300
- subkey ,
301
- self .s3_bucket ,
302
- self .s3_data_key ,
303
- subkey ,
304
- )
305
- if not self .args .dry_run :
306
- s3 .put_object (
307
- Bucket = self .s3_bucket ,
308
- Body = fd ,
309
- ServerSideEncryption = "AES256" ,
310
- Key = self .s3_data_key + subkey ,
311
- )
312
-
313
- if self .s3_template_key is not None :
314
- logging .info (
315
- "Upload template to %s:%s" , self .s3_bucket , self .s3_template_key
316
- )
317
- if not self .args .dry_run :
318
- s3 .put_object (
319
- Bucket = self .s3_bucket ,
320
- Body = stack .body .encode ("utf-8" ),
321
- ServerSideEncryption = "AES256" ,
322
- Key = self .s3_template_key ,
323
- )
404
+ self ._upload_stack (stack )
324
405
325
406
logging .info ("Validate template for stack %s" % stack .name )
326
407
if not self .args .dry_run :
@@ -334,6 +415,7 @@ def execute_for_stack(self, stack: Stack, aws_env: Session | None = None) -> int
334
415
return self ._push_stack_changeset (
335
416
stack = stack , s3_template_url = self .s3_template_url
336
417
)
418
+
337
419
elif self .args .command == "show" :
338
420
print (stack .body )
339
421
elif self .args .command == "protect" :
@@ -428,19 +510,31 @@ def execute(
428
510
return 1
429
511
430
512
return_val = 0
431
- stacks = self .create_stack ()
432
-
433
- if isinstance (stacks , list ):
434
- for stack in stacks :
435
- return_val = self .execute_for_stack (stack , aws_env = aws_env )
436
- # Stop at first failure
437
- if return_val :
438
- return return_val
439
- else :
440
- return_val = self .execute_for_stack (stacks , aws_env = aws_env )
441
513
514
+ # Create a temporary assets dir here as assets need to be generated at the
515
+ # time of create_stack
516
+ with tempfile .TemporaryDirectory () as temp_assets_dir :
517
+ self .gen_assets_dir = temp_assets_dir
518
+ self .pre_create_stack ()
519
+ stacks = self .create_stack ()
520
+ self .post_create_stack ()
521
+
522
+ if isinstance (stacks , list ):
523
+ for stack in stacks :
524
+ return_val = self .execute_for_stack (stack , aws_env = aws_env )
525
+ # Stop at first failure
526
+ if return_val :
527
+ return return_val
528
+ else :
529
+ return_val = self .execute_for_stack (stacks , aws_env = aws_env )
530
+
531
+ self .gen_assets_dir = None
442
532
return return_val
443
533
534
+ def pre_create_stack (self ) -> None :
535
+ """Before create_stack."""
536
+ pass
537
+
444
538
@abc .abstractmethod
445
539
def create_stack (self ) -> Stack | list [Stack ]:
446
540
"""Create a stack.
@@ -449,6 +543,10 @@ def create_stack(self) -> Stack | list[Stack]:
449
543
"""
450
544
pass
451
545
546
+ def post_create_stack (self ) -> None :
547
+ """After create_stack."""
548
+ pass
549
+
452
550
@property
453
551
def stack_policy_body (self ) -> str | None :
454
552
"""Stack Policy that can be set by calling the command ``protect``.
0 commit comments