Skip to content

Commit 1bed588

Browse files
committed
updated auto tag terraform logic
minor version increase
1 parent d6de1ff commit 1bed588

File tree

3 files changed

+128
-77
lines changed

3 files changed

+128
-77
lines changed

package/cloudshell/iac/terraform/services/tf_proc_exec.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(self, shell_helper: ShellHelperObject, sb_data_handler: SandboxData
3434

3535
def init_terraform(self):
3636
self._shell_helper.logger.info("Performing Terraform Init...")
37-
self._shell_helper.sandbox_messages.write_message("Running Terraform Init...")
37+
self._shell_helper.sandbox_messages.write_message("running Terraform Init...")
3838

3939
self._backend_handler.generate_backend_cfg_file()
4040
backend_config_vars = self._backend_handler.get_backend_secret_vars()
@@ -117,7 +117,8 @@ def tag_terraform(self) -> None:
117117

118118
terraform_version = self._shell_helper.attr_handler.get_attribute(ATTRIBUTE_NAMES.TERRAFORM_VERSION)
119119

120-
start_tagging_terraform_resources(self._tf_working_dir, self._shell_helper.logger, tags_dict, inputs_dict, terraform_version)
120+
start_tagging_terraform_resources(self._tf_working_dir, self._shell_helper.logger, tags_dict, inputs_dict,
121+
terraform_version)
121122
self._set_service_status("Progress 40", "Tagging Passed")
122123
except Exception:
123124
self._set_service_status("Offline", "Tagging Failed")

package/cloudshell/iac/terraform/tagging/tag_terraform_resources.py

Lines changed: 124 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,18 @@
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
2018
import re
2119
import enum
22-
import sys
2320
import traceback
2421
from typing import List
2522
import hcl2
@@ -33,38 +30,44 @@
3330
### Added
3431
from cloudshell.iac.terraform.models.exceptions import TerraformAutoTagsError
3532

33+
3634
# =====================================================================================================================
3735

38-
#commented out some contants and methods but isnt required
3936
class 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
106109
class 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
188182
class 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):
383380
class 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
596594
def _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\nErrors 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)

package/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.2.0
1+
1.2.1

0 commit comments

Comments
 (0)