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 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} can not be destroyed for any reason')
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 deletion in changes['unauthorized']:
73
- printer.verbose(f" - {deletion}")
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
- for resource in detected_changes[key]:
135
+
136
+ for index, resource in enumerate(detected_changes[key]):
118
137
  resource_address = resource["address"]
119
-
120
- has_match, resource_config = get_matching_dd_config(resource_address, dd_config)
121
- if has_match:
122
- # so what drift classification do we have?
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
- changes['drifts']['unknown'] += 1
127
- if resource_address not in changes['unknowns']:
128
- changes['unknowns'].append(resource_address)
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
- changes['drifts']['total'] += 1
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
- return changes, (not changes['unauthorized'])
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/--include-external-dependencies', '-x/-i',
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('--manifest-file', '-m',
705
- help='Manifest file describing the deployment options',
706
- required=True, default="manifest.yaml", show_default=True,
707
- type=click.Path(),
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(manifest_file, verbose, working_dir, out):
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
- manifest_file=manifest_file,
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, manifest, target_dir, version_tag=None):
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'Deploy using reference {version_tag}')
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 not analyze_config:
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
- for key, value in details.items():
1358
- for type in ["minor", "medium", "major", "unknown", "total"]:
1359
- total_drifts[type] += value["drifts"][type]
1360
-
1361
- # the formula below is just a way to achieve a numeric results that is coming from the various drift categories
1362
- value['drifts']['score'] = calculate_score(
1363
- major = value['drifts']['major'],
1364
- medium = value['drifts']['medium'],
1365
- minor = value['drifts']['minor'],
1366
- )
1367
- value['drifts']['score'] = value['drifts']['major'] * 10 + value['drifts']['medium'] + value['drifts']['minor'] / 10
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
- # the formula below is just a way to achieve a numeric results that is coming from the various drift categories
1370
- total_drift_score = total_drifts['major'] * 10 + total_drifts['medium'] + total_drifts['minor'] / 10
1371
- total_drifts['score'] = total_drift_score
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
- self.printer.header(f"Drift score: {total_drift_score} ({total_drifts['major']}.{total_drifts['medium']}.{total_drifts['minor']})")
1374
- if total_drifts["unknown"] > 0:
1375
- self.printer.warning(f"For {total_drifts['unknown']} resources, drift score is not configured, please update configuration!")
1376
- self.printer.warning('- Unknowns:')
1377
- for key, value in details.items():
1378
- for m in value["unknowns"]:
1379
- self.printer.warning(f' -> {m}')
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
- manifest=manifest,
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, manifest_file, working_dir, out):
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
- if os.path.isdir(full_entry) and level < self.LOCATE_VERSION_FILE_MAX_LEVELS:
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(os.path.relpath(current_directory, root_directory))
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 get_all_version(repo_dir, min_version=None):
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 ({self.LOCATE_VERSION_FILE_MAX_LEVELS} levels) in directory: {working_dir}')
1752
+ self.printer.header(f'Check released versions (max {levels_deep} levels deep) in directory: {working_dir}')
1694
1753
 
1695
- result = locate_version_files(working_dir)
1754
+ found_files = locate_version_files(working_dir)
1696
1755
 
1697
1756
  versions = []
1698
- for location in result:
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, location, self.VERSION_FILE), 'r') as file:
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': location,
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({location: 'unknown'})
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
- manifest = self.load_yaml_file(os.path.join(working_dir, manifest_file))
1730
- self._clone_repo(manifest=manifest, target_dir=temp_dir)
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 = get_all_version(repo_dir=temp_dir, min_version=min_version['tag'])
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 > 60:
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 < 7:
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.10.3
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,,
@@ -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,,