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
@@ -239,6 +245,19 @@ def parse_value(self, value):
239245 parsed_value = value
240246 return str (parsed_value ).strip ()
241247
248+ def cleanup_ssm_parameters (self ):
249+ local_value_keys = set (self .convert_flatten (self .local_values ).keys () or [])
250+ remote_value_keys = set (self .remote_ssm_parameters .keys () or [])
251+
252+ orphaned_ssm_parameters = remote_value_keys - local_value_keys
253+
254+ for parameter in orphaned_ssm_parameters :
255+ if parameter_record := self .remote_ssm_parameters .get (parameter ):
256+ self .ssm_client .delete_parameter (
257+ Name = parameter_record ["Name" ]
258+ )
259+ logger .info (f"Deleted orphaned ssm parameter { parameter } " )
260+
242261 def cleanup_secrets (self ):
243262 local_secret_keys = self .convert_flatten (self .local_secrets ).keys ()
244263 remote_secret_keys = self .remote_secret_records .keys ()
@@ -292,6 +311,13 @@ def remote_secret_records(self) -> typing.Dict[str, Any]:
292311
293312 return self ._remote_secrets
294313
314+ @property
315+ def remote_ssm_parameters (self ) -> typing .Dict [str , Any ]:
316+ if not hasattr (self , "_remote_parameters" ):
317+ self ._remote_parameters = self .get_remote_ssm_parameters ()
318+
319+ return self ._remote_parameters
320+
295321 @property
296322 def remote_values (self ) -> typing .Dict [str , Any ]:
297323 if not hasattr (self , "_remote_values" ):
@@ -315,12 +341,33 @@ def get_remote_params(self, flatten=True):
315341 app_secrets = self .remote_secrets
316342 return {** app_values , ** app_secrets }
317343
344+ def get_flattened_parameters (self ) -> typing .Dict [str , Any ]:
345+ return self .convert_flatten (self .local_values )
346+
318347 def get_flattened_secrets (self ) -> typing .Dict [str , Any ]:
319348 return self .convert_flatten (self .local_secrets )
349+
350+ def get_parameter_id (self , parameter ):
351+ return f"/{ self .get_secret_id (parameter )} "
320352
321353 def get_secret_id (self , secret ):
322354 return os .path .join ("" , self .environment .name , self .name , secret )
323355
356+ def put_parameter (self , parameter_id , parameter_value , tags = None , type = "String" , overwrite = True ):
357+ print (f"Creating Parameter { parameter_id } " )
358+ self .ssm_client .put_parameter (
359+ Name = parameter_id ,
360+ Value = parameter_value ,
361+ Type = type ,
362+ Overwrite = overwrite ,
363+ )
364+ if tags :
365+ self .ssm_client .add_tags_to_resource (
366+ ResourceType = "Parameter" ,
367+ ResourceId = parameter_id ,
368+ Tags = tags
369+ )
370+
324371 def create_secret (self , Name , SecretString , Tags , ForceOverwriteReplicaSecret ):
325372 print (f"Creating Secret { Name } " )
326373 try :
@@ -358,6 +405,31 @@ def backoff_with_custom_exception(self, func, exception, message_prefix="", max_
358405 print (f"Retrying in { delay } seconds..." )
359406 time .sleep (delay )
360407
408+ def push_parameters (self ):
409+ for parameter , value in self .get_flattened_parameters ().items ():
410+ parameter_value = str (value )
411+ if (value_size := sys .getsizeof (parameter_value )) > 4096 :
412+ self .environment .manager .click .secho (
413+ f"Parameter: { parameter } value is too large to store ({ value_size } )"
414+ )
415+ continue
416+ parameter_id = self .get_parameter_id (parameter )
417+ self .backoff_with_custom_exception (
418+ self .put_parameter ,
419+ exception = botocore .exceptions .ClientError ,
420+ message_prefix = f"Creating/Updating parameter { parameter_id } " ,
421+ max_attempts = 5 ,
422+ base_delay = 1 ,
423+ max_delay = 10 ,
424+ factor = 2 ,
425+ parameter_id = parameter_id ,
426+ parameter_value = parameter_value ,
427+ tags = [
428+ {"Key" : "Environment" , "Value" : self .environment .name },
429+ {"Key" : "App" , "Value" : self .name },
430+ ],
431+ )
432+
361433 def push_secrets (self ):
362434 for secret , value in self .get_flattened_secrets ().items ():
363435 sec_val = str (value )
@@ -396,6 +468,16 @@ def fetch_secret_value(self, secret):
396468 return ""
397469 return response ["SecretString" ]
398470
471+ @property
472+ def remote_ssm_parameter_filters (self ):
473+ return [
474+ {
475+ "Key" : "Name" ,
476+ "Option" : "Contains" ,
477+ "Values" : [f"/{ self .environment .name } /{ self .name } " ]
478+ }
479+ ]
480+
399481 @property
400482 def remote_secret_filters (self ):
401483 return [
@@ -405,6 +487,20 @@ def remote_secret_filters(self):
405487 {"Key" : "tag-value" , "Values" : [self .name ]},
406488 ]
407489
490+ def get_remote_ssm_parameters (self ):
491+ paginator = self .ssm_client .get_paginator ('describe_parameters' )
492+ parameters = {}
493+ filters = self .remote_ssm_parameter_filters
494+ response = paginator .paginate (
495+ ParameterFilters = filters ,
496+ )
497+ for page in response :
498+ for parameter in page ["Parameters" ]:
499+ parameter_key = os .path .split (parameter ["Name" ])[- 1 ]
500+ # parameters.append(parameter_key)
501+ parameters [parameter_key ] = parameter
502+ return parameters
503+
408504 def get_remote_secrets (self ) -> typing .Dict [str , str ]:
409505 paginator = self .secrets_client .get_paginator ("list_secrets" )
410506 secrets = {}
@@ -511,6 +607,16 @@ def s3_client(self) -> S3Client:
511607 "s3" , region_name = self .region , endpoint_url = self .endpoint_url
512608 )
513609 return self ._s3_client
610+
611+ @property
612+ def ssm_client (self ):
613+ if not hasattr (self , '_ssm_client' ):
614+ self ._ssm_client = boto3 .client (
615+ "ssm" ,
616+ region_name = self .region ,
617+ endpoint_url = self .endpoint_url
618+ )
619+ return self ._ssm_client
514620
515621 @property
516622 def secrets_client (self ) -> SecretsManagerClient :
@@ -555,14 +661,20 @@ def initBootstrap(self):
555661 self .click .secho (f"S3 Client Error { err } " , bg = "red" , fg = "white" )
556662
557663 def put_config (self , delete_first ):
558-
664+ self . cleanup_ssm_parameters ()
559665 self .cleanup_secrets ()
560666 for _environment_name , environment in self .environments .items ():
561667 for _app_name , app in environment .apps .items ():
562668 # pass
563669 app .upload_to_s3 ()
670+ app .push_parameters ()
564671 app .push_secrets ()
565672
673+ def cleanup_ssm_parameters (self ):
674+ for _environment_name , environment in self .environments .items ():
675+ for _app_name , app in environment .apps .items ():
676+ app .cleanup_ssm_parameters ()
677+
566678 def cleanup_secrets (self ):
567679 for _environment_name , environment in self .environments .items ():
568680 for _app_name , app in environment .apps .items ():
0 commit comments