55# - remove/comment out main (only uses start_tagging_terraform_resources function)
66# - add "from cloudshell.iac.terraform.models.exceptions import TerraformAutoTagsError"
77# - verify imports are the same (need to add to dependencies file if different and require specific version)
8- # - modify method definition params
98# - modify logger to use logger from module
109# - _perform_terraform_init_plan is heavily changed due to the fact we may need to run this on windows or linux
1110
1211# modified methods:
13-
14- # - _perform_terraform_init_plan (x2)
15- # - start_tagging_terraform_resources
1612# - init_logging
13+ # - start_tagging_terraform_resources
14+ # - _perform_terraform_init_plan
1715# - OverrideTagsTemplatesCreator
1816
19-
17+ import argparse
2018import re
2119import enum
22- import sys
2320import traceback
2421from typing import List
2522import hcl2
3330### Added
3431from cloudshell .iac .terraform .models .exceptions import TerraformAutoTagsError
3532
33+
3634# =====================================================================================================================
3735
38- #commented out some contants and methods but isnt required
3936class Constants :
4037 TAGS = "tags" # used for tag aws and azure resources in terraform
4138 LABELS = "labels" # used for tag kubernetes resources in terraform
42- # TORQUE_VARIABLES_FILE_NAME = "variables.torque.tfvars"
43- # TORQUE_TAGS_FILE_NAME = "torque_tags.json"
4439 OVERRIDE_LOG_FILE_NAME = "override_log"
45- EXCLUDE_FROM_TAGGING_FILE_NAME = "exclude_from_tagging.json"
46- # TERRAFORM_FOLDER_NAME = "terraform"
47-
48- # @staticmethod
49- # def get_tfs_folder_path(main_folder: str):
50- # return os.path.join(main_folder,
51- # Constants.TERRAFORM_FOLDER_NAME)
52-
53- # @staticmethod
54- # def get_torque_tags_path(main_folder: str):
55- # return os.path.join(main_folder,
56- # Constants.TORQUE_TAGS_FILE_NAME)
40+ EXCLUDE_FROM_TAGGING_FILE_NAME = "exclude_from_tagging.json" # modified
5741
5842 @staticmethod
5943 def get_override_log_path (main_folder : str ):
6044 return os .path .join (main_folder ,
6145 Constants .OVERRIDE_LOG_FILE_NAME )
6246
47+ # modified
6348 @staticmethod
6449 def get_exclude_from_tagging_file_path (main_folder : str ):
6550 return os .path .join (main_folder ,
6651 Constants .EXCLUDE_FROM_TAGGING_FILE_NAME )
6752
53+ # =====================================================================================================================
54+
55+
56+ class Settings :
57+ def __init__ (self ,
58+ tf_exe_path : str = "" ,
59+ tf_version : str = "" ,
60+ tf_module_dir_path : str = "" ,
61+ tf_vars_file_path : str = "" ,
62+ tags_file_path : str = "" ,
63+ exclude_from_tagging_file_path : str = "" ):
64+ self .tf_exe_path = tf_exe_path
65+ self .tf_version = tf_version
66+ self .tf_module_dir_path = tf_module_dir_path
67+ self .tf_vars_file_path = tf_vars_file_path
68+ self .tags_file_path = tags_file_path
69+ self .exclude_from_tagging_file_path = exclude_from_tagging_file_path
70+
6871
6972# =====================================================================================================================
7073
@@ -102,44 +105,35 @@ def wrapper(*args, **kwargs):
102105
103106# =====================================================================================================================
104107
105- #modified
108+ # modified
106109class LoggerHelper :
107110 log_instance = None
108111
109112 @staticmethod
110113 def init_logging (logger : logging .Logger ):
111114 LoggerHelper .log_instance = logger
112115
113- @staticmethod
114- def actual_write (log_type : str , logger : logging .Logger , msg : str , code_line : int = None ):
115- try :
116- if code_line is None :
117- caller = getframeinfo (stack ()[1 ][0 ])
118- code_line = caller .lineno
119- if log_type == "info" :
120- logger .info (f" Line { code_line } ]: { msg } " )
121- elif log_type == "warning" :
122- logger .warning (f" Line { code_line } ]: { msg } " )
123- elif log_type == "error" :
124- logger .error (f" Line { code_line } ]: { msg } " )
125- else :
126- raise ValueError ('unknown logtype' )
127-
128- # logging is nice to have but we don't want an error with the log to interfere with the code flow
129- except Exception as e :
130- print (e )
131-
132116 @staticmethod
133117 def write_info (msg : str , code_line : int = None ):
134- LoggerHelper .actual_write ("info" , LoggerHelper .log_instance , msg , code_line )
118+ if code_line is None :
119+ caller = getframeinfo (stack ()[1 ][0 ])
120+ code_line = caller .lineno
121+ LoggerHelper .log_instance .info (f" Line { code_line } ]: { msg } " )
135122
136123 @staticmethod
137124 def write_warning (msg : str , code_line : int = None ):
138- LoggerHelper .actual_write ("warning" , LoggerHelper .log_instance , msg , code_line )
125+ if code_line is None :
126+ caller = getframeinfo (stack ()[1 ][0 ])
127+ code_line = caller .lineno
128+ LoggerHelper .log_instance .warning (f" Line { code_line } ]: { msg } " )
139129
140130 @staticmethod
141131 def write_error (msg : str , code_line : int = None ):
142- LoggerHelper .actual_write ("error" , LoggerHelper .log_instance , msg , code_line )
132+ if code_line is None :
133+ caller = getframeinfo (stack ()[1 ][0 ])
134+ code_line = caller .lineno
135+ LoggerHelper .log_instance .error (f" Line { code_line } ]: { msg } " )
136+
143137
144138
145139# =====================================================================================================================
@@ -183,7 +177,7 @@ def __init__(self, resource_type: str, resource_name: str, tags):
183177
184178# =====================================================================================================================
185179
186- #modified
180+ # modified
187181@ExceptionWrapper .wrap_class
188182class OverrideTagsTemplatesCreator :
189183 def __init__ (self , tags_dict : dict , terraform_version : str ):
@@ -194,6 +188,9 @@ def __init__(self, tags_dict: dict, terraform_version: str):
194188 self ._terraform_syntax = TerraformSyntaxVersion .get_terraform_syntax (terraform_version )
195189 self ._map_key_value_separator = self ._get_map_key_value_separator ()
196190
191+ if not self .torque_tags_dict :
192+ LoggerHelper .write_error (f"Didn't get tags dict, exiting the tagging process" )
193+ return
197194 # if os.path.exists(self.torque_tags_file_path):
198195 # LoggerHelper.write_info(f"Reading torque tags from \"{self.torque_tags_file_path}\"")
199196 # self._read_and_save_tags_from_file()
@@ -383,8 +380,14 @@ def get_terraform_syntax(terraform_version: str):
383380class Hcl2Parser :
384381 @staticmethod
385382 def get_tf_file_as_dict (tf_file_path : str ) -> dict :
386- with (open (tf_file_path , 'r' )) as client_tf_file :
387- return hcl2 .load (client_tf_file )
383+ try :
384+ with (open (tf_file_path , 'r' )) as client_tf_file :
385+ return hcl2 .load (client_tf_file )
386+ except :
387+ # logging the file path that encountered the error
388+ LoggerHelper .write_error (f"Failed to parse tf file '{ tf_file_path } '" )
389+ # re-raising the exception so it will break the flow and its details are logged by the ExceptionWrapper
390+ raise
388391
389392 # full_resource_object contain many information in an unreadable structure
390393 # so we convert it to more less info with more readable structure
@@ -400,20 +403,14 @@ def get_terraform_resource_safely(full_resource_object: dict) -> TerraformResour
400403
401404 tags = full_resource_object [resource_type ][resource_name ].get ('tags' , None )
402405
403- # When hcl2 parse a tf file it return all the tags blocks in a resource .
404- # But a valid tf file (according to terraform) allow only one tags block in a resource.
405- # So even if the hcl2 will return many tags blocks we will only be working with the first of them.
406- # But because we run 'terraform init' and 'terraform plan' before we run this py file
407- # we can be sure that if we have a tags block in the resource then we have only one
408- # (because otherwise 'terraform plan' command would have return an error)
406+ # before version 3.0.0 of hcl2, "tags" was a list and we took the first element in it
407+ # this behavior was changed here: https://github.com/amplify-education/python-hcl2/blob/master/CHANGELOG.md#300---2021-07-14
409408 if tags :
410409 # we replace ' with " becuase hc2l has some bug:
411410 # for example it parse --> merge(local.common_tags, {"Name"="tomer"})
412411 # to --> merge(local.common_tags, {'Name'='tomer'}) and this is invalid syntax according to terraform
413- if type (tags [0 ]) is str :
414- tags = tags [0 ].replace ("'" , "\" " ).replace (",," , "," ) # due to bug in hcl2 library
415- else :
416- tags = tags [0 ]
412+ if type (tags ) is str :
413+ tags = tags .replace ("'" , "\" " ).replace (",," , "," ) # due to bug in hcl2 library
417414
418415 return TerraformResource (resource_type = resource_type ,
419416 resource_name = resource_name ,
@@ -592,17 +589,16 @@ def create_override_file(self, untaggable_resources_types: List[str] = []):
592589# M A I N
593590# =====================================================================================================================
594591
592+
595593# modified
596594def _perform_terraform_init_plan (main_tf_dir_path : str , inputs_dict : dict ):
597-
598595 inputs = []
596+ for input_key , input_value in inputs_dict .items ():
597+ inputs .extend (['-var' , f'{ input_key } ={ input_value } ' ])
599598
600- for inputkey , inputvalue in inputs_dict .items ():
601- inputs .extend (['-var' , f'{ inputkey } ={ inputvalue } ' ])
602-
603- executable_cmd = f'{ os .path .join (main_tf_dir_path , "terraform.exe" )} '
604- init_command = [executable_cmd , 'init' , '-no-color' ]
605- plan_command = [executable_cmd , 'plan' , '-no-color' , '-input=false' ]
599+ terraform_exe_path = f'{ os .path .join (main_tf_dir_path , "terraform.exe" )} '
600+ init_command = [terraform_exe_path , 'init' , '-no-color' ]
601+ plan_command = [terraform_exe_path , 'plan' , '-no-color' , '-input=false' ]
606602 plan_command .extend (inputs )
607603
608604 init = subprocess .Popen (init_command , cwd = main_tf_dir_path , stdout = subprocess .PIPE , stderr = subprocess .PIPE )
@@ -625,33 +621,37 @@ def _get_untaggable_resources_types_from_plan_output(text: str) -> List[str]:
625621 RegexHelper .UNSUPPORTED_TAGS_OR_LABELS_PATTERN_2 ])
626622 return untaggable_resources
627623
628- #modified
629- def start_tagging_terraform_resources (main_dir_path : str , logger , tags_dict : dict , inputs_dict : dict = dict (), terraform_version : str = "" ):
624+
625+ # modified
626+ def start_tagging_terraform_resources (main_dir_path : str , logger , tags_dict : dict , inputs_dict : dict = None ,
627+ terraform_version : str = "" ):
630628 if not os .path .exists (main_dir_path ):
631629 raise TerraformAutoTagsError (f"Path { main_dir_path } does not exist" )
632630 tfs_folder_path = main_dir_path
633- # log_path = Constants.get_override_log_path(main_dir_path )
634- # torque_tags_file_path = Constants.get_torque_tags_path(main_dir_path)
631+ # log_path = Constants.get_override_log_path(settings.tf_module_dir_path )
632+ # torque_tags_file_path = settings.tags_file_path
635633 exclude_from_tagging_file_path = Constants .get_exclude_from_tagging_file_path (main_dir_path )
634+ # tf_exe_path = settings.tf_exe_path
635+ # tf_vars_file_path = settings.tf_vars_file_path
636636
637637 # modified
638638 LoggerHelper .init_logging (logger )
639639
640640 LoggerHelper .write_info (f"Trying to preform terraform init & plan in the directory '{ tfs_folder_path } '"
641641 " in order to check for any validation errors in tf files" )
642- stdout , stderr , return_code = _perform_terraform_init_plan (tfs_folder_path , inputs_dict )
642+ stdout , stderr , return_code = _perform_terraform_init_plan (tfs_folder_path , inputs_dict ) # modified
643643 if return_code != 0 or stderr :
644644 LoggerHelper .write_error ("Exit before the override procedure began because the init/plan failed."
645645 f" (Return_code is { return_code } )"
646646 f"\n \n Errors are:\n { stderr } \n " )
647647 # Error Code 3 mark to the outside tf script (that run me) that there was an error but not because of
648648 # the override procedure (but because the client tf file has compile errors even before we started the
649649 # override procedure)
650- raise TerraformAutoTagsError ( "Validation errors during Terraform Init/Plan when applying automated tags" )
650+ exit ( 3 )
651651 LoggerHelper .write_info (f"terraform init & plan passed successfully" )
652652
653- #modified
654- tags_templates_creator = OverrideTagsTemplatesCreator (tags_dict , terraform_version = terraform_version )
653+ # modified
654+ tags_templates_creator = OverrideTagsTemplatesCreator (tags_dict , terraform_version )
655655
656656 all_tf_files = FilesHelper .get_all_files (tfs_folder_path , ".tf" )
657657 all_tf_files_without_overrides = [file for file in all_tf_files if not file .file_name .endswith ("_override.tf" )]
@@ -672,7 +672,6 @@ def start_tagging_terraform_resources(main_dir_path: str, logger, tags_dict: dic
672672
673673 LoggerHelper .write_info (f"Trying to preform terraform init & plan in the directory '{ tfs_folder_path } '"
674674 " in order to check for any untaggable resources in the override files" )
675-
676675 # modified
677676 # Check (by analyzing the terraform plan output) to see if any of the override files
678677 # has a "tags/labels" that was assigned to untaggable resources
@@ -732,8 +731,59 @@ def _validate_terraform_version_arg(terraform_version_arg: str) -> bool:
732731 if not terraform_version_arg :
733732 return False
734733
735- version_arr = terraform_version .split ("." )
734+ version_arr = terraform_version_arg .split ("." )
736735 if len (version_arr ) != 3 :
737736 return False
738737
739738 return version_arr [0 ].isdigit () and version_arr [1 ].isdigit ()
739+
740+
741+ # def _try_get_settings() -> Settings:
742+ # parser = argparse.ArgumentParser()
743+ #
744+ # # mandatory positional arguments
745+ # parser.add_argument('tf_version',
746+ # help="TF version, e.g. 0.10.0, 0.12.0, 0.15.0, ...")
747+ # parser.add_argument('tf_module_dir_path',
748+ # help="path to the folder with the tf files")
749+ # parser.add_argument('tags_file_path',
750+ # help="path to the json file with the tags to be applied to the module")
751+ #
752+ # # optional named arguments
753+ # parser.add_argument('--tf_exe_path', default="",
754+ # help="path to the TF executable, e.g. /usr/bin/terraform. "
755+ # "if not specified, PATH locations will be searched for 'terraform' exe")
756+ # parser.add_argument('--tf_vars_file_path', default="",
757+ # help="path to the vars file for the module")
758+ # parser.add_argument('--exclude_from_tagging_file_path', default="",
759+ # help="path to the json file with the resources to be excluded from tagging")
760+ #
761+ # settings = Settings()
762+ # # if invalid, will print error + usage and exit
763+ # parser.parse_args(namespace=settings)
764+ #
765+ # # additional validations
766+ # if settings.tf_exe_path and not os.path.isfile(settings.tf_exe_path):
767+ # parser.print_usage()
768+ # print("error: tf_exe_path value is invalid")
769+ # exit(1)
770+ #
771+ # if not _validate_terraform_version_arg(settings.tf_version):
772+ # parser.print_usage()
773+ # print("error: tf_version value is invalid")
774+ # exit(1)
775+ #
776+ # if not os.path.isdir(settings.tf_module_dir_path):
777+ # parser.print_usage()
778+ # print("error: tf_module_dir_path value is invalid")
779+ # exit(1)
780+ #
781+ # return settings
782+ #
783+ #
784+ # modified
785+ # if __name__ == '__main__':
786+ #
787+ # parsed_settings = _try_get_settings()
788+ #
789+ # start_tagging_terraform_resources(settings=parsed_settings)
0 commit comments