tgwrap 0.10.3__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tgwrap/analyze.py +60 -16
- tgwrap/cli.py +20 -8
- tgwrap/main.py +134 -57
- {tgwrap-0.10.3.dist-info → tgwrap-0.11.0.dist-info}/METADATA +3 -1
- tgwrap-0.11.0.dist-info/RECORD +13 -0
- tgwrap-0.10.3.dist-info/RECORD +0 -13
- {tgwrap-0.10.3.dist-info → tgwrap-0.11.0.dist-info}/LICENSE +0 -0
- {tgwrap-0.10.3.dist-info → tgwrap-0.11.0.dist-info}/WHEEL +0 -0
- {tgwrap-0.10.3.dist-info → tgwrap-0.11.0.dist-info}/entry_points.txt +0 -0
tgwrap/analyze.py
CHANGED
@@ -9,7 +9,7 @@ import re
|
|
9
9
|
|
10
10
|
from .printer import Printer
|
11
11
|
|
12
|
-
def run_analyze(config, data, verbose=False):
|
12
|
+
def run_analyze(config, data, ignore_attributes, verbose=False):
|
13
13
|
""" Run the terrasafe validation and return the unauthorized deletions """
|
14
14
|
|
15
15
|
printer = Printer(verbose)
|
@@ -44,11 +44,29 @@ def run_analyze(config, data, verbose=False):
|
|
44
44
|
for criticallity, ts_level in ts_default_levels.items():
|
45
45
|
ts_config[ts_level] = [f"*{key}*" for key, value in config[criticallity].items() if value.get('terrasafe_level', ts_level) == ts_level]
|
46
46
|
|
47
|
+
ignorable_updates = []
|
48
|
+
for resource in detected_changes['updates']:
|
49
|
+
before = resource['change']['before']
|
50
|
+
after = resource['change']['after']
|
51
|
+
|
52
|
+
for i in ignore_attributes:
|
53
|
+
try:
|
54
|
+
before.pop(i)
|
55
|
+
except KeyError:
|
56
|
+
pass
|
57
|
+
try:
|
58
|
+
after.pop(i)
|
59
|
+
except KeyError:
|
60
|
+
pass
|
61
|
+
|
62
|
+
if before == after:
|
63
|
+
ignorable_updates.append(resource['address'])
|
64
|
+
|
47
65
|
for resource in detected_changes['deletions']:
|
48
66
|
resource_address = resource["address"]
|
49
67
|
# Deny has precendence over Allow!
|
50
68
|
if is_resource_match_any(resource_address, ts_config["unauthorized_deletion"]):
|
51
|
-
printer.verbose(f'Resource {resource_address}
|
69
|
+
printer.verbose(f'Resource {resource_address} cannot be destroyed for any reason')
|
52
70
|
elif is_resource_match_any(resource_address, ts_config["ignore_deletion"]):
|
53
71
|
continue
|
54
72
|
elif is_resource_recreate(resource) and is_resource_match_any(
|
@@ -69,8 +87,8 @@ def run_analyze(config, data, verbose=False):
|
|
69
87
|
|
70
88
|
if changes['unauthorized']:
|
71
89
|
printer.verbose("Unauthorized deletion detected for those resources:")
|
72
|
-
for
|
73
|
-
printer.verbose(f" - {
|
90
|
+
for resource in changes['unauthorized']:
|
91
|
+
printer.verbose(f" - {resource}")
|
74
92
|
printer.verbose("If you really want to delete those resources: comment it or export this environment variable:")
|
75
93
|
printer.verbose(f"export TERRASAFE_ALLOW_DELETION=\"{';'.join(changes['unauthorized'])}\"")
|
76
94
|
|
@@ -114,20 +132,26 @@ def run_analyze(config, data, verbose=False):
|
|
114
132
|
|
115
133
|
# now we have the proper lists, calculate the drifts
|
116
134
|
for key, value in {'deletions': 'delete', 'creations': 'create', 'updates': 'update'}.items():
|
117
|
-
|
135
|
+
|
136
|
+
for index, resource in enumerate(detected_changes[key]):
|
118
137
|
resource_address = resource["address"]
|
119
|
-
|
120
|
-
|
121
|
-
if
|
122
|
-
|
123
|
-
dd_class = resource_config[value]
|
124
|
-
changes['drifts'][dd_class] += 1
|
138
|
+
|
139
|
+
# updates might need to be ignored
|
140
|
+
if key == 'updates' and resource_address in ignorable_updates:
|
141
|
+
detected_changes[key].pop(index)
|
125
142
|
else:
|
126
|
-
|
127
|
-
|
128
|
-
|
143
|
+
has_match, resource_config = get_matching_dd_config(resource_address, dd_config)
|
144
|
+
|
145
|
+
if has_match:
|
146
|
+
# so what drift classification do we have?
|
147
|
+
dd_class = resource_config[value]
|
148
|
+
changes['drifts'][dd_class] += 1
|
149
|
+
else:
|
150
|
+
changes['drifts']['unknown'] += 1
|
151
|
+
if resource_address not in changes['unknowns']:
|
152
|
+
changes['unknowns'].append(resource_address)
|
129
153
|
|
130
|
-
|
154
|
+
changes['drifts']['total'] += 1
|
131
155
|
|
132
156
|
# remove ballast from the following lists
|
133
157
|
changes['all'] = [ # ignore read and no-ops
|
@@ -140,9 +164,17 @@ def run_analyze(config, data, verbose=False):
|
|
140
164
|
]
|
141
165
|
changes['creations'] = [resource["address"] for resource in detected_changes['creations']]
|
142
166
|
changes['updates'] = [resource["address"] for resource in detected_changes['updates']]
|
167
|
+
changes['ignorable_updates'] = ignorable_updates
|
143
168
|
|
144
|
-
|
169
|
+
# see if there are output changes
|
170
|
+
output_changes = get_output_changes(data)
|
171
|
+
changes['outputs'] = []
|
172
|
+
relevant_changes = set(['create', 'update', 'delete'])
|
173
|
+
for k,v in output_changes.items():
|
174
|
+
if relevant_changes.intersection(v['actions']):
|
175
|
+
changes['outputs'].append(k)
|
145
176
|
|
177
|
+
return changes, (not changes['unauthorized'])
|
146
178
|
|
147
179
|
def parse_ignored_from_env_var():
|
148
180
|
ignored = os.environ.get("TERRASAFE_ALLOW_DELETION")
|
@@ -170,6 +202,18 @@ def get_resource_actions(data):
|
|
170
202
|
|
171
203
|
return changes
|
172
204
|
|
205
|
+
def get_output_changes(data):
|
206
|
+
# check format version
|
207
|
+
if data["format_version"].split(".")[0] != "0" and data["format_version"].split(".")[0] != "1":
|
208
|
+
raise Exception("Only format major version 0 or 1 is supported")
|
209
|
+
|
210
|
+
if "output_changes" in data:
|
211
|
+
output_changes = data["output_changes"]
|
212
|
+
else:
|
213
|
+
output_changes = {}
|
214
|
+
|
215
|
+
return output_changes
|
216
|
+
|
173
217
|
|
174
218
|
def has_delete_action(resource):
|
175
219
|
return "delete" in resource["change"]["actions"]
|
tgwrap/cli.py
CHANGED
@@ -366,7 +366,7 @@ def run_import(address, id, verbose, dry_run, working_dir, no_lock, terragrunt_a
|
|
366
366
|
help='Verbose printing',
|
367
367
|
show_default=True
|
368
368
|
)
|
369
|
-
@click.option('--exclude-external-dependencies
|
369
|
+
@click.option('--exclude-external-dependencies', '-x',
|
370
370
|
is_flag=True, default=True,
|
371
371
|
help='Whether or not external dependencies must be ignored',
|
372
372
|
show_default=True
|
@@ -388,6 +388,11 @@ def run_import(address, id, verbose, dry_run, working_dir, no_lock, terragrunt_a
|
|
388
388
|
@click.option('--parallel-execution', '-p', is_flag=True, default=False,
|
389
389
|
help='Whether or not to use parallel execution',
|
390
390
|
)
|
391
|
+
@click.option('--ignore-attributes', '-i',
|
392
|
+
multiple=True, default=[],
|
393
|
+
help=r'A glob of attributes for which updates can be ignored, this option can be used multiple times.',
|
394
|
+
show_default=True
|
395
|
+
)
|
391
396
|
@click.option('--include-dir', '-I',
|
392
397
|
multiple=True, default=[],
|
393
398
|
help=r'A glob of a directory that needs to be included, this option can be used multiple times. For example: -I "integrations/\*/\*"',
|
@@ -411,7 +416,7 @@ def run_import(address, id, verbose, dry_run, working_dir, no_lock, terragrunt_a
|
|
411
416
|
@click.argument('terragrunt-args', nargs=-1, type=click.UNPROCESSED)
|
412
417
|
@click.version_option(version=__version__)
|
413
418
|
def run_analyze(verbose, exclude_external_dependencies, working_dir, start_at_step,
|
414
|
-
out, analyze_config, parallel_execution, include_dir, exclude_dir,
|
419
|
+
out, analyze_config, parallel_execution, ignore_attributes, include_dir, exclude_dir,
|
415
420
|
planfile_dir, data_collection_endpoint, terragrunt_args):
|
416
421
|
""" Analyzes the plan files """
|
417
422
|
|
@@ -425,6 +430,7 @@ def run_analyze(verbose, exclude_external_dependencies, working_dir, start_at_st
|
|
425
430
|
out=out,
|
426
431
|
analyze_config=analyze_config,
|
427
432
|
parallel_execution=parallel_execution,
|
433
|
+
ignore_attributes=ignore_attributes,
|
428
434
|
include_dirs=include_dir,
|
429
435
|
exclude_dirs=exclude_dir,
|
430
436
|
planfile_dir=planfile_dir,
|
@@ -701,10 +707,15 @@ def deploy(
|
|
701
707
|
ignore_unknown_options=True,
|
702
708
|
),
|
703
709
|
)
|
704
|
-
@click.option('--
|
705
|
-
help='
|
706
|
-
required=True,
|
707
|
-
|
710
|
+
@click.option('--platform-repo-url', '-p',
|
711
|
+
help='URL of the platform git repository',
|
712
|
+
required=True,
|
713
|
+
envvar='TGWRAP_PLATFORM_REPO_URL'
|
714
|
+
)
|
715
|
+
@click.option('--levels-deep', '-l',
|
716
|
+
help='For how many (directory) levels deep must be searched for deployments',
|
717
|
+
required=True, default=5, show_default=True,
|
718
|
+
type=int,
|
708
719
|
)
|
709
720
|
@click.option('--verbose', '-v',
|
710
721
|
help='Verbose printing',
|
@@ -718,14 +729,15 @@ def deploy(
|
|
718
729
|
show_default=True
|
719
730
|
)
|
720
731
|
@click.version_option(version=__version__)
|
721
|
-
def check_deployments(
|
732
|
+
def check_deployments(platform_repo_url, levels_deep, verbose, working_dir, out):
|
722
733
|
""" Check the freshness of deployed configuration versions against the platform repository """
|
723
734
|
|
724
735
|
check_latest_version(verbose)
|
725
736
|
|
726
737
|
tgwrap = TgWrap(verbose=verbose)
|
727
738
|
tgwrap.check_deployments(
|
728
|
-
|
739
|
+
repo_url=platform_repo_url,
|
740
|
+
levels_deep=levels_deep,
|
729
741
|
working_dir=working_dir,
|
730
742
|
out=out,
|
731
743
|
)
|
tgwrap/main.py
CHANGED
@@ -53,7 +53,6 @@ class TgWrap():
|
|
53
53
|
TERRAGRUNT_FILE='terragrunt.hcl'
|
54
54
|
VERSION_FILE="version.hcl"
|
55
55
|
LATEST_VERSION='latest'
|
56
|
-
LOCATE_VERSION_FILE_MAX_LEVELS=3
|
57
56
|
PLANFILE_NAME="planfile"
|
58
57
|
TG_SOURCE_VAR="TERRAGRUNT_SOURCE"
|
59
58
|
TG_SOURCE_MAP_VAR="TERRAGRUNT_SOURCE_MAP"
|
@@ -375,7 +374,7 @@ class TgWrap():
|
|
375
374
|
|
376
375
|
return graph
|
377
376
|
|
378
|
-
def _clone_repo(self,
|
377
|
+
def _clone_repo(self, repo, target_dir, version_tag=None):
|
379
378
|
"""Clones the repo, possibly a specific version, into a temp directory"""
|
380
379
|
|
381
380
|
def get_tags(target_dir):
|
@@ -455,9 +454,7 @@ class TgWrap():
|
|
455
454
|
return is_latest, is_branch, is_tag
|
456
455
|
|
457
456
|
# clone the repo
|
458
|
-
repo = manifest['git_repository']
|
459
457
|
self.printer.verbose(f'Clone repo {repo}')
|
460
|
-
|
461
458
|
cmd = f"git clone {repo} {target_dir}"
|
462
459
|
rc = subprocess.run(
|
463
460
|
shlex.split(cmd),
|
@@ -486,7 +483,7 @@ class TgWrap():
|
|
486
483
|
working_dir=target_dir,
|
487
484
|
)
|
488
485
|
|
489
|
-
self.printer.header(f'
|
486
|
+
self.printer.header(f'Fetch repo using reference {version_tag}')
|
490
487
|
|
491
488
|
if is_latest:
|
492
489
|
pass # nothing to do, we already have latest
|
@@ -1208,7 +1205,7 @@ class TgWrap():
|
|
1208
1205
|
self.printer.verbose(rc)
|
1209
1206
|
|
1210
1207
|
def analyze(self, exclude_external_dependencies, working_dir, start_at_step,
|
1211
|
-
out, analyze_config, parallel_execution, include_dirs, exclude_dirs,
|
1208
|
+
out, analyze_config, parallel_execution, ignore_attributes, include_dirs, exclude_dirs,
|
1212
1209
|
planfile_dir, data_collection_endpoint, terragrunt_args):
|
1213
1210
|
""" Analyzes the plan files """
|
1214
1211
|
|
@@ -1247,11 +1244,7 @@ class TgWrap():
|
|
1247
1244
|
cmd = f"terraform show -json {self.PLANFILE_NAME}"
|
1248
1245
|
|
1249
1246
|
config = None
|
1250
|
-
if
|
1251
|
-
self.printer.warning(
|
1252
|
-
f"Analyze config file is not set, this is required for checking for unauthorized deletions and drift detection scores!"
|
1253
|
-
)
|
1254
|
-
else:
|
1247
|
+
if analyze_config:
|
1255
1248
|
self.printer.verbose(
|
1256
1249
|
f"\nAnalyze using config {analyze_config}"
|
1257
1250
|
)
|
@@ -1259,7 +1252,6 @@ class TgWrap():
|
|
1259
1252
|
|
1260
1253
|
ts_validation_successful = True
|
1261
1254
|
details = {}
|
1262
|
-
drifts = {}
|
1263
1255
|
try:
|
1264
1256
|
# then run it and capture the output
|
1265
1257
|
with tempfile.NamedTemporaryFile(mode='w+', prefix='tgwrap-', delete=False) as f:
|
@@ -1290,7 +1282,6 @@ class TgWrap():
|
|
1290
1282
|
except IndexError:
|
1291
1283
|
self.printer.warning(f'Could not determine planfile: {line[:100]}')
|
1292
1284
|
|
1293
|
-
|
1294
1285
|
try:
|
1295
1286
|
# plan file could be empty (except for new line) if module is skipped
|
1296
1287
|
if len(plan_file) > 1:
|
@@ -1304,7 +1295,9 @@ class TgWrap():
|
|
1304
1295
|
config=config,
|
1305
1296
|
data=data,
|
1306
1297
|
verbose=self.printer.print_verbose,
|
1298
|
+
ignore_attributes=ignore_attributes,
|
1307
1299
|
)
|
1300
|
+
|
1308
1301
|
if not ts_success:
|
1309
1302
|
ts_validation_successful = False
|
1310
1303
|
else:
|
@@ -1321,6 +1314,7 @@ class TgWrap():
|
|
1321
1314
|
"creations": 0,
|
1322
1315
|
"updates": 0,
|
1323
1316
|
"deletions": 0,
|
1317
|
+
"outputs": 0,
|
1324
1318
|
"minor": 0,
|
1325
1319
|
"medium": 0,
|
1326
1320
|
"major": 0,
|
@@ -1331,9 +1325,14 @@ class TgWrap():
|
|
1331
1325
|
|
1332
1326
|
self.printer.header("Analysis results:", print_line_before=True)
|
1333
1327
|
for key, value in details.items():
|
1328
|
+
# if we want to ignore a few attributes
|
1329
|
+
if ignore_attributes:
|
1330
|
+
value['updates'] = [item for item in value['updates'] if item not in value['ignorable_updates']]
|
1331
|
+
|
1334
1332
|
self.printer.header(f'Module: {key}')
|
1335
|
-
if not value["all"]:
|
1333
|
+
if not value["all"] and not value["outputs"]:
|
1336
1334
|
self.printer.success('No changes detected')
|
1335
|
+
|
1337
1336
|
if value["unauthorized"]:
|
1338
1337
|
self.printer.error('Unauthorized deletions:')
|
1339
1338
|
for m in value["unauthorized"]:
|
@@ -1353,30 +1352,48 @@ class TgWrap():
|
|
1353
1352
|
for m in value["updates"]:
|
1354
1353
|
total_drifts["updates"] = total_drifts["updates"] + 1
|
1355
1354
|
self.printer.normal(f'-> {m}')
|
1355
|
+
if value["ignorable_updates"]:
|
1356
|
+
if self.printer.print_verbose:
|
1357
|
+
self.printer.normal('Updates (ignored):')
|
1358
|
+
for m in value["ignorable_updates"]:
|
1359
|
+
self.printer.normal(f'-> {m}')
|
1360
|
+
else:
|
1361
|
+
self.printer.normal(f'Updates (ignored): {len(value["ignorable_updates"])} resources (add --verbose to see them)')
|
1362
|
+
if value["outputs"]:
|
1363
|
+
self.printer.normal('Output changes:')
|
1364
|
+
for m in value["outputs"]:
|
1365
|
+
total_drifts["outputs"] = total_drifts["outputs"] + 1
|
1366
|
+
self.printer.normal(f'-> {m}')
|
1356
1367
|
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
+
if not analyze_config:
|
1369
|
+
self.printer.error(
|
1370
|
+
f"Analyze config file is not set, this is required for checking for unauthorized deletions and drift detection scores!",
|
1371
|
+
print_line_before=True,
|
1372
|
+
)
|
1373
|
+
else:
|
1374
|
+
for key, value in details.items():
|
1375
|
+
for type in ["minor", "medium", "major", "unknown", "total"]:
|
1376
|
+
total_drifts[type] += value["drifts"][type]
|
1377
|
+
|
1378
|
+
# the formula below is just a way to achieve a numeric results that is coming from the various drift categories
|
1379
|
+
value['drifts']['score'] = calculate_score(
|
1380
|
+
major = value['drifts']['major'],
|
1381
|
+
medium = value['drifts']['medium'],
|
1382
|
+
minor = value['drifts']['minor'],
|
1383
|
+
)
|
1384
|
+
value['drifts']['score'] = value['drifts']['major'] * 10 + value['drifts']['medium'] + value['drifts']['minor'] / 10
|
1368
1385
|
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1386
|
+
# the formula below is just a way to achieve a numeric results that is coming from the various drift categories
|
1387
|
+
total_drift_score = total_drifts['major'] * 10 + total_drifts['medium'] + total_drifts['minor'] / 10
|
1388
|
+
total_drifts['score'] = total_drift_score
|
1372
1389
|
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1390
|
+
self.printer.header(f"Drift score: {total_drift_score} ({total_drifts['major']}.{total_drifts['medium']}.{total_drifts['minor']})")
|
1391
|
+
if total_drifts["unknown"] > 0:
|
1392
|
+
self.printer.warning(f"For {total_drifts['unknown']} resources, drift score is not configured, please update configuration!")
|
1393
|
+
self.printer.warning('- Unknowns:')
|
1394
|
+
for key, value in details.items():
|
1395
|
+
for m in value["unknowns"]:
|
1396
|
+
self.printer.warning(f' -> {m}')
|
1380
1397
|
|
1381
1398
|
if out or data_collection_endpoint:
|
1382
1399
|
# in the output we convert the dict of dicts to a list of dicts as it makes processing
|
@@ -1514,7 +1531,7 @@ class TgWrap():
|
|
1514
1531
|
source_config_dir = None
|
1515
1532
|
|
1516
1533
|
version_tag, _, _ = self._clone_repo(
|
1517
|
-
|
1534
|
+
repo=manifest['git_repository'],
|
1518
1535
|
target_dir=temp_dir,
|
1519
1536
|
version_tag=version_tag,
|
1520
1537
|
)
|
@@ -1636,30 +1653,72 @@ class TgWrap():
|
|
1636
1653
|
except Exception:
|
1637
1654
|
pass
|
1638
1655
|
|
1639
|
-
def check_deployments(self,
|
1656
|
+
def check_deployments(self, repo_url, levels_deep, working_dir, out):
|
1640
1657
|
""" Check the freshness of deployed configuration versions against the platform repository """
|
1641
1658
|
|
1642
|
-
def locate_version_files(current_directory, found_files=[], root_directory=None, level=1):
|
1659
|
+
def locate_version_files(current_directory, found_files=[], root_directory=None, level=1, git_status=''):
|
1643
1660
|
" This tries to find a version file in the current directory, or a given number of directories beneath it"
|
1644
1661
|
|
1662
|
+
# do not include hidden directories
|
1663
|
+
if os.path.basename(current_directory).startswith('.'):
|
1664
|
+
return found_files
|
1665
|
+
|
1645
1666
|
if not root_directory:
|
1646
1667
|
root_directory = current_directory
|
1647
1668
|
|
1669
|
+
if not git_status:
|
1670
|
+
self.printer.verbose(f'Check for git status in directory {current_directory}')
|
1671
|
+
# Execute 'git status' to get an overview of the current status
|
1672
|
+
cmd = "git status"
|
1673
|
+
rc = subprocess.run(
|
1674
|
+
shlex.split(cmd),
|
1675
|
+
cwd=current_directory,
|
1676
|
+
universal_newlines=True,
|
1677
|
+
stdout=subprocess.PIPE,
|
1678
|
+
stderr=subprocess.PIPE,
|
1679
|
+
)
|
1680
|
+
output = ('stdout: ' + rc.stdout + 'stderr: ' + rc.stderr).lower()
|
1681
|
+
if 'not a git repository' in output:
|
1682
|
+
pass
|
1683
|
+
elif 'branch is up to date' in output:
|
1684
|
+
git_status = 'up to date; '
|
1685
|
+
elif 'head detached' in output:
|
1686
|
+
git_status = 'head detached; '
|
1687
|
+
elif 'untracked files' in output:
|
1688
|
+
git_status = git_status + 'untracked files; '
|
1689
|
+
elif 'changes to be committed' in output:
|
1690
|
+
git_status = git_status + 'staged changes; '
|
1691
|
+
elif 'changes not staged for commit' in output:
|
1692
|
+
git_status = git_status + 'unstaged changes; '
|
1693
|
+
elif 'branch is ahead of' in output:
|
1694
|
+
git_status = git_status + 'ahead of remote; '
|
1695
|
+
elif 'branch is behind of' in output:
|
1696
|
+
git_status = git_status + 'behind remote; '
|
1697
|
+
elif 'unmerged paths' in output:
|
1698
|
+
git_status = git_status + 'merge conflicts; '
|
1699
|
+
|
1648
1700
|
for entry in os.listdir(current_directory):
|
1649
1701
|
full_entry = os.path.join(current_directory, entry)
|
1650
|
-
|
1702
|
+
|
1703
|
+
if os.path.isdir(full_entry) and level <= levels_deep:
|
1651
1704
|
found_files = locate_version_files(
|
1652
1705
|
current_directory=full_entry,
|
1653
1706
|
found_files=found_files,
|
1654
1707
|
root_directory=root_directory,
|
1655
1708
|
level=level+1,
|
1709
|
+
git_status=git_status,
|
1656
1710
|
)
|
1657
1711
|
elif entry == self.VERSION_FILE:
|
1658
|
-
found_files.append(
|
1712
|
+
found_files.append(
|
1713
|
+
{
|
1714
|
+
'path': os.path.relpath(current_directory, root_directory),
|
1715
|
+
'git_status': git_status,
|
1716
|
+
}
|
1717
|
+
)
|
1659
1718
|
|
1660
1719
|
return found_files
|
1661
1720
|
|
1662
|
-
def
|
1721
|
+
def get_all_versions(repo_dir, min_version=None):
|
1663
1722
|
"Get all the version tags from the repo including their data"
|
1664
1723
|
|
1665
1724
|
# Execute 'git tag' command to get a list of all tags
|
@@ -1690,26 +1749,27 @@ class TgWrap():
|
|
1690
1749
|
try:
|
1691
1750
|
# do we have a working dir?
|
1692
1751
|
working_dir = working_dir if working_dir else os.getcwd()
|
1693
|
-
self.printer.header(f'Check released versions ({
|
1752
|
+
self.printer.header(f'Check released versions (max {levels_deep} levels deep) in directory: {working_dir}')
|
1694
1753
|
|
1695
|
-
|
1754
|
+
found_files = locate_version_files(working_dir)
|
1696
1755
|
|
1697
1756
|
versions = []
|
1698
|
-
for
|
1757
|
+
for result in found_files:
|
1699
1758
|
# Determine the deployed version as defined in the version file
|
1700
|
-
with open(os.path.join(working_dir,
|
1759
|
+
with open(os.path.join(working_dir, result['path'], self.VERSION_FILE), 'r') as file:
|
1701
1760
|
# todo: replace this with regex as it is (now) the only reason we use this lib
|
1702
1761
|
content = hcl2.load(file)
|
1703
1762
|
try:
|
1704
1763
|
version_tag = content['locals'][0]['version_tag']
|
1705
1764
|
versions.append(
|
1706
1765
|
{
|
1707
|
-
'path':
|
1766
|
+
'path': result['path'],
|
1767
|
+
'git_status': result['git_status'],
|
1708
1768
|
'tag': version_tag
|
1709
1769
|
}
|
1710
1770
|
)
|
1711
1771
|
except KeyError as e:
|
1712
|
-
versions.append({
|
1772
|
+
versions.append({result: 'unknown'})
|
1713
1773
|
|
1714
1774
|
self.printer.verbose(f'Detected versions: {versions}')
|
1715
1775
|
|
@@ -1726,11 +1786,14 @@ class TgWrap():
|
|
1726
1786
|
self.printer.verbose(f'Detected minimum version {min_version} and maximum version {max_version}')
|
1727
1787
|
|
1728
1788
|
temp_dir = os.path.join(tempfile.mkdtemp(prefix='tgwrap-'), "tg-source")
|
1729
|
-
|
1730
|
-
|
1789
|
+
self._clone_repo(
|
1790
|
+
repo=repo_url,
|
1791
|
+
target_dir=temp_dir,
|
1792
|
+
version_tag='latest',
|
1793
|
+
)
|
1731
1794
|
|
1732
1795
|
# determine the version tag from the repo, including their date
|
1733
|
-
all_versions =
|
1796
|
+
all_versions = get_all_versions(repo_dir=temp_dir, min_version=min_version['tag'])
|
1734
1797
|
|
1735
1798
|
# so now we can determine how old the deployed versions are
|
1736
1799
|
now = datetime.now(timezone.utc)
|
@@ -1744,31 +1807,45 @@ class TgWrap():
|
|
1744
1807
|
version['days_since_release'] = (now - release_date).days
|
1745
1808
|
|
1746
1809
|
self.printer.header(
|
1747
|
-
'Deployed versions:' if len(versions) > 0 else 'No deployed versions detected'
|
1810
|
+
'Deployed versions:' if len(versions) > 0 else 'No deployed versions detected',
|
1811
|
+
print_line_before=True,
|
1748
1812
|
)
|
1749
|
-
|
1813
|
+
|
1814
|
+
# sort the list based on its path
|
1815
|
+
versions = sorted(versions, key=lambda x: x['path'])
|
1816
|
+
|
1750
1817
|
for version in versions:
|
1751
1818
|
days_since_release = version.get("days_since_release", 0)
|
1752
1819
|
message = f'-> {version["path"]}: {version["tag"]} (released {days_since_release} days ago)'
|
1753
1820
|
if version['release_date'] == 'unknown':
|
1754
1821
|
self.printer.normal(message)
|
1755
|
-
elif days_since_release >
|
1756
|
-
self.printer.error(message)
|
1757
|
-
elif days_since_release > 30:
|
1822
|
+
elif days_since_release > 120:
|
1758
1823
|
self.printer.error(message)
|
1759
|
-
elif days_since_release
|
1824
|
+
elif days_since_release > 80:
|
1825
|
+
self.printer.warning(message)
|
1826
|
+
elif days_since_release < 40:
|
1760
1827
|
self.printer.success(message)
|
1761
1828
|
else:
|
1762
1829
|
self.printer.normal(message)
|
1763
1830
|
|
1831
|
+
if version.get('git_status'):
|
1832
|
+
message = f'WARNING: git status: {version["git_status"].strip()}'
|
1833
|
+
if not 'up to date' in message:
|
1834
|
+
self.printer.warning(message)
|
1835
|
+
|
1764
1836
|
self.printer.normal("\n") # just to get an empty line :-/
|
1765
1837
|
self.printer.warning("""
|
1766
1838
|
Note:
|
1767
1839
|
This result only says something about the freshness of the deployed configurations,
|
1768
1840
|
but not whether the actual resources are in sync with these.
|
1841
|
+
|
1769
1842
|
Check the drift of these configurations with the actual deployments by
|
1770
1843
|
planning and analyzing the results.
|
1771
|
-
|
1844
|
+
|
1845
|
+
Also, it uses the locally checked out repositories, make sure these are pulled so that
|
1846
|
+
this reflect the most up to date situation!
|
1847
|
+
""",
|
1848
|
+
print_line_before=True, print_line_after=True)
|
1772
1849
|
|
1773
1850
|
if out:
|
1774
1851
|
# use the regular printer, to avoid it being sent to stderr
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: tgwrap
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.11.0
|
4
4
|
Summary: A (terragrunt) wrapper around a (terraform) wrapper around ....
|
5
5
|
Home-page: https://gitlab.com/lunadata/tgwrap
|
6
6
|
License: MIT
|
@@ -72,6 +72,8 @@ And this was not something a bunch of aliases could solve, hence this wrapper wa
|
|
72
72
|
|
73
73
|
When using the run-all, analyzing what is about to be changed is not going to be easier. Hence we created the `tgwrap analyze` function that lists all the planned changes and (if a config file is availabe) calculates a drift score and runs a [terrasafe](https://pypi.org/project/terrasafe/) style validation check.
|
74
74
|
|
75
|
+
> you can ignore minor changes, such as tag updates, with `tgwrap analyze -i tags`
|
76
|
+
|
75
77
|
It needs a config file as follows:
|
76
78
|
|
77
79
|
```yaml
|
@@ -0,0 +1,13 @@
|
|
1
|
+
tgwrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
tgwrap/analyze.py,sha256=5C0OcDpOfn6U39QNIoc7V9ePjjcXKbJH6cOVNr9jJ9A,10289
|
3
|
+
tgwrap/cli.py,sha256=RYZf7Mb7qJW-q_-qqREi9s2dAu_wpOtvmvB086Y5URQ,32084
|
4
|
+
tgwrap/deploy.py,sha256=-fSk-Ix_HqrXY7KQX_L27TnFzIuhBHYv4xYJW6zRDN4,10243
|
5
|
+
tgwrap/inspector-resources-template.yml,sha256=Mos8NDzzZ3VxdXgeiVL9cmQfRcIXIHMLf79_KLwdXu8,3297
|
6
|
+
tgwrap/inspector.py,sha256=5pW7Ex1lkKRoXY6hZGbCNmSD2iRzgMSfqi9w7gb-AcY,16990
|
7
|
+
tgwrap/main.py,sha256=rCdrsu1J4biQj7IDqF0AaR-s7DL-cQ2DTt50zAfjZ8M,93702
|
8
|
+
tgwrap/printer.py,sha256=frn1PARd8A28mkRCYR6ybN2x0NBULhNOutn4l2U7REY,2754
|
9
|
+
tgwrap-0.11.0.dist-info/LICENSE,sha256=VT-AVxIXt3EQTC-7Hy1uPGnrDNJLqfcgLgJD78fiyx4,1065
|
10
|
+
tgwrap-0.11.0.dist-info/METADATA,sha256=10rGGUHsLlUTxMGsbhltzdi8bd2QNHkmE_uQ9fHMs8Q,17560
|
11
|
+
tgwrap-0.11.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
12
|
+
tgwrap-0.11.0.dist-info/entry_points.txt,sha256=H8X0PMPmd4aW7Y9iyChZ0Ug6RWGXqhRUvHH-6f6Mxz0,42
|
13
|
+
tgwrap-0.11.0.dist-info/RECORD,,
|
tgwrap-0.10.3.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
tgwrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
tgwrap/analyze.py,sha256=TJvAKVIbWl8-8oxpTwXVBWU72q_XQKzUTlyMZ25cV2M,8728
|
3
|
-
tgwrap/cli.py,sha256=3tf-wi-zN6T_0T2fHt8UtWzoZeUwlraGWsu5BduYxs4,31646
|
4
|
-
tgwrap/deploy.py,sha256=-fSk-Ix_HqrXY7KQX_L27TnFzIuhBHYv4xYJW6zRDN4,10243
|
5
|
-
tgwrap/inspector-resources-template.yml,sha256=Mos8NDzzZ3VxdXgeiVL9cmQfRcIXIHMLf79_KLwdXu8,3297
|
6
|
-
tgwrap/inspector.py,sha256=5pW7Ex1lkKRoXY6hZGbCNmSD2iRzgMSfqi9w7gb-AcY,16990
|
7
|
-
tgwrap/main.py,sha256=Ggnb9S1GbsZUzg4TW_9xUhK2ZaZUCoAWX6oBteRu4O4,90012
|
8
|
-
tgwrap/printer.py,sha256=frn1PARd8A28mkRCYR6ybN2x0NBULhNOutn4l2U7REY,2754
|
9
|
-
tgwrap-0.10.3.dist-info/LICENSE,sha256=VT-AVxIXt3EQTC-7Hy1uPGnrDNJLqfcgLgJD78fiyx4,1065
|
10
|
-
tgwrap-0.10.3.dist-info/METADATA,sha256=K47ImnV2gqCBkW6ARwFyuNiAJWJkyGEyxiUZXeIXJmc,17476
|
11
|
-
tgwrap-0.10.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
12
|
-
tgwrap-0.10.3.dist-info/entry_points.txt,sha256=H8X0PMPmd4aW7Y9iyChZ0Ug6RWGXqhRUvHH-6f6Mxz0,42
|
13
|
-
tgwrap-0.10.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|