11from __future__ import annotations
22
3+ import sys
4+
35from typing import TYPE_CHECKING , Any
46
57import botocore .exceptions
@@ -194,6 +196,10 @@ def __init__(self, name, path, environment: BootstrapEnvironment):
194196 def s3_client (self ) -> S3Client :
195197 return self .environment .manager .s3_client
196198
199+ @property
200+ def ssm_client (self ) -> SSMClient :
201+ return self .environment .manager .ssm_client
202+
197203 @property
198204 def secrets_client (self ) -> SecretsManagerClient :
199205 return self .environment .manager .secrets_client
@@ -243,6 +249,24 @@ def parse_value(self, value):
243249 parsed_value = value
244250 return str (parsed_value ).strip ()
245251
252+ def cleanup_ssm_parameters (self ):
253+ local_value_keys = set (self .convert_flatten (self .local_values ).keys () or [])
254+ self .raw = True
255+ remote_value_keys = set (self .remote_values or [])
256+ self .raw = None
257+
258+ orphaned_ssm_parameters = remote_value_keys - local_value_keys
259+
260+ for parameter in orphaned_ssm_parameters :
261+ parameter_id = self .get_parameter_id (parameter )
262+ try :
263+ self .ssm_client .delete_parameter (
264+ Name = self .get_parameter_id (parameter )
265+ )
266+ logger .info (f"Deleted orphaned ssm parameter { parameter } " )
267+ except Exception :
268+ logger .info (f"Parameter: { parameter_id } could not be deleted" )
269+
246270 def cleanup_secrets (self ):
247271 local_secret_keys = self .convert_flatten (self .local_secrets ).keys ()
248272 remote_secret_keys = self .remote_secret_records .keys ()
@@ -296,6 +320,13 @@ def remote_secret_records(self) -> typing.Dict[str, Any]:
296320
297321 return self ._remote_secrets
298322
323+ @property
324+ def remote_ssm_parameters (self ) -> typing .Dict [str , Any ]:
325+ if not hasattr (self , "_remote_parameters" ):
326+ self ._remote_parameters = self .get_remote_ssm_parameters ()
327+
328+ return self ._remote_parameters
329+
299330 @property
300331 def remote_values (self ) -> typing .Dict [str , Any ]:
301332 if not hasattr (self , "_remote_values" ):
@@ -325,12 +356,33 @@ def get_remote_params(self, flatten=True):
325356 app_secrets = self .remote_secrets
326357 return {** app_values , ** app_secrets }
327358
359+ def get_flattened_parameters (self ) -> typing .Dict [str , Any ]:
360+ return self .convert_flatten (self .local_values )
361+
328362 def get_flattened_secrets (self ) -> typing .Dict [str , Any ]:
329363 return self .convert_flatten (self .local_secrets )
364+
365+ def get_parameter_id (self , parameter ):
366+ return f"/{ self .get_secret_id (parameter )} "
330367
331368 def get_secret_id (self , secret ):
332369 return os .path .join ("" , self .environment .name , self .name , secret )
333370
371+ def put_parameter (self , parameter_id , parameter_value , tags = None , type = "String" , overwrite = True ):
372+ print (f"Creating Parameter { parameter_id } " )
373+ self .ssm_client .put_parameter (
374+ Name = parameter_id ,
375+ Value = parameter_value ,
376+ Type = type ,
377+ Overwrite = overwrite ,
378+ )
379+ if tags :
380+ self .ssm_client .add_tags_to_resource (
381+ ResourceType = "Parameter" ,
382+ ResourceId = parameter_id ,
383+ Tags = tags
384+ )
385+
334386 def create_secret (self , Name , SecretString , Tags , ForceOverwriteReplicaSecret ):
335387 print (f"Creating Secret { Name } " )
336388 try :
@@ -368,6 +420,31 @@ def backoff_with_custom_exception(self, func, exception, message_prefix="", max_
368420 print (f"Retrying in { delay } seconds..." )
369421 time .sleep (delay )
370422
423+ def push_parameters (self ):
424+ for parameter , value in self .get_flattened_parameters ().items ():
425+ parameter_value = str (value )
426+ if (value_size := sys .getsizeof (parameter_value )) > 4096 or not parameter_value :
427+ self .environment .manager .click .secho (
428+ f"Parameter: { parameter } value is too large to store ({ value_size } )"
429+ )
430+ continue
431+ parameter_id = self .get_parameter_id (parameter )
432+ self .backoff_with_custom_exception (
433+ self .put_parameter ,
434+ exception = botocore .exceptions .ClientError ,
435+ message_prefix = f"Creating/Updating parameter { parameter_id } " ,
436+ max_attempts = 5 ,
437+ base_delay = 1 ,
438+ max_delay = 10 ,
439+ factor = 2 ,
440+ parameter_id = parameter_id ,
441+ parameter_value = parameter_value ,
442+ tags = [
443+ {"Key" : "Environment" , "Value" : self .environment .name },
444+ {"Key" : "App" , "Value" : self .name },
445+ ],
446+ )
447+
371448 def push_secrets (self ):
372449 for secret , value in self .get_flattened_secrets ().items ():
373450 sec_val = str (value )
@@ -406,6 +483,16 @@ def fetch_secret_value(self, secret):
406483 return ""
407484 return response ["SecretString" ]
408485
486+ @property
487+ def remote_ssm_parameter_filters (self ):
488+ return [
489+ {
490+ "Key" : "Name" ,
491+ "Option" : "Contains" ,
492+ "Values" : [f"/{ self .environment .name } /{ self .name } " ]
493+ }
494+ ]
495+
409496 @property
410497 def remote_secret_filters (self ):
411498 return [
@@ -415,6 +502,20 @@ def remote_secret_filters(self):
415502 {"Key" : "tag-value" , "Values" : [self .name ]},
416503 ]
417504
505+ def get_remote_ssm_parameters (self ):
506+ paginator = self .ssm_client .get_paginator ('describe_parameters' )
507+ parameters = {}
508+ filters = self .remote_ssm_parameter_filters
509+ response = paginator .paginate (
510+ ParameterFilters = filters ,
511+ )
512+ for page in response :
513+ for parameter in page ["Parameters" ]:
514+ parameter_key = os .path .split (parameter ["Name" ])[- 1 ]
515+ # parameters.append(parameter_key)
516+ parameters [parameter_key ] = parameter
517+ return parameters
518+
418519 def get_remote_secrets (self ) -> typing .Dict [str , str ]:
419520 paginator = self .secrets_client .get_paginator ("list_secrets" )
420521 secrets = {}
@@ -521,6 +622,16 @@ def s3_client(self) -> S3Client:
521622 "s3" , region_name = self .region , endpoint_url = self .endpoint_url
522623 )
523624 return self ._s3_client
625+
626+ @property
627+ def ssm_client (self ):
628+ if not hasattr (self , '_ssm_client' ):
629+ self ._ssm_client = boto3 .client (
630+ "ssm" ,
631+ region_name = self .region ,
632+ endpoint_url = self .endpoint_url
633+ )
634+ return self ._ssm_client
524635
525636 @property
526637 def secrets_client (self ) -> SecretsManagerClient :
@@ -565,14 +676,20 @@ def initBootstrap(self):
565676 self .click .secho (f"S3 Client Error { err } " , bg = "red" , fg = "white" )
566677
567678 def put_config (self , delete_first ):
568-
679+ self . cleanup_ssm_parameters ()
569680 self .cleanup_secrets ()
570681 for _environment_name , environment in self .environments .items ():
571682 for _app_name , app in environment .apps .items ():
572683 # pass
573684 app .upload_to_s3 ()
685+ app .push_parameters ()
574686 app .push_secrets ()
575687
688+ def cleanup_ssm_parameters (self ):
689+ for _environment_name , environment in self .environments .items ():
690+ for _app_name , app in environment .apps .items ():
691+ app .cleanup_ssm_parameters ()
692+
576693 def cleanup_secrets (self ):
577694 for _environment_name , environment in self .environments .items ():
578695 for _app_name , app in environment .apps .items ():
0 commit comments