tgwrap 0.8.13__py3-none-any.whl → 0.9.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/cli.py CHANGED
@@ -292,9 +292,10 @@ def run_all(command, verbose, debug, dry_run, no_lock, update, upgrade, exclude_
292
292
  @click.option('--json', '-j', is_flag=True, default=False,
293
293
  help='Show output in json format',
294
294
  )
295
- @click.option('--planfile-dir', '-P', default=None,
295
+ @click.option('--planfile-dir', '-P', default='.terragrunt-cache/current',
296
296
  help='Relative path to directory with plan file (or set TGWRAP_PLANFILE_DIR environment variable), see README for more details',
297
297
  envvar='TGWRAP_PLANFILE_DIR', type=click.Path(),
298
+ show_default=True,
298
299
  )
299
300
  @click.argument('terragrunt-args', nargs=-1, type=click.UNPROCESSED)
300
301
  @click.version_option(version=__version__)
@@ -402,11 +403,16 @@ def run_import(address, id, verbose, dry_run, working_dir, no_lock, terragrunt_a
402
403
  envvar='TGWRAP_PLANFILE_DIR', type=click.Path(),
403
404
  show_default=True,
404
405
  )
406
+ @click.option('--data-collection-endpoint', '-D', default=None,
407
+ help='Optional URI of an (Azure) data collection endpoint, to which the analyse results will be sent',
408
+ envvar='TGWRAP_ANALYZE_DATA_COLLECTION_ENDPOINT',
409
+ show_default=True,
410
+ )
405
411
  @click.argument('terragrunt-args', nargs=-1, type=click.UNPROCESSED)
406
412
  @click.version_option(version=__version__)
407
413
  def run_analyze(verbose, exclude_external_dependencies, working_dir, start_at_step,
408
- out, analyze_config, parallel_execution,
409
- include_dir, exclude_dir, planfile_dir, terragrunt_args):
414
+ out, analyze_config, parallel_execution, include_dir, exclude_dir,
415
+ planfile_dir, data_collection_endpoint, terragrunt_args):
410
416
  """ Analyzes the plan files """
411
417
 
412
418
  check_latest_version(verbose)
@@ -422,6 +428,7 @@ def run_analyze(verbose, exclude_external_dependencies, working_dir, start_at_st
422
428
  include_dirs=include_dir,
423
429
  exclude_dirs=exclude_dir,
424
430
  planfile_dir=planfile_dir,
431
+ data_collection_endpoint=data_collection_endpoint,
425
432
  terragrunt_args=terragrunt_args,
426
433
  )
427
434
 
tgwrap/deploy.py CHANGED
@@ -179,11 +179,18 @@ def prepare_deploy_config(step, config, source_dir, source_config_dir, target_di
179
179
  source_path = os.path.join(
180
180
  source_dir, source_stage, substack['source'], ''
181
181
  )
182
+ source_modules = {
183
+ entry:{} for entry in os.listdir(source_path) if os.path.isdir(os.path.join(source_path, entry))
184
+ }
182
185
  target_path = os.path.join(
183
186
  target_dir, substack['target'], ''
184
187
  )
185
188
 
186
- include_modules = substack['include_modules'] if len(substack.get('include_modules', {})) > 0 else []
189
+ printer.verbose(f'Include substack modules: {include_modules}')
190
+
191
+ # printer.verbose(f'Found modules: {source_modules}')
192
+ # include_modules = config['include_modules'] if len(config.get('include_modules')) > 0 else source_modules
193
+ include_modules = substack['include_modules'] if len(substack.get('include_modules', {})) > 0 else source_modules
187
194
  printer.verbose(f'Include modules: {include_modules}')
188
195
 
189
196
  if include_modules:
tgwrap/main.py CHANGED
@@ -17,6 +17,7 @@ import sys
17
17
  import subprocess
18
18
  import shlex
19
19
  import shutil
20
+ import requests
20
21
  import re
21
22
  import tempfile
22
23
  import json
@@ -34,13 +35,13 @@ from datetime import datetime, timezone
34
35
  from .printer import Printer
35
36
  from .analyze import run_analyze
36
37
  from .deploy import prepare_deploy_config, run_sync
38
+
37
39
  class DateTimeEncoder(json.JSONEncoder):
38
40
  def default(self, obj):
39
41
  if isinstance(obj, datetime):
40
42
  return obj.isoformat()
41
43
  return super().default(obj)
42
44
 
43
-
44
45
  class TgWrap():
45
46
  """
46
47
  A wrapper around terragrunt with the sole purpose to make it a bit
@@ -775,6 +776,150 @@ class TgWrap():
775
776
  total_items = sum(len(group) for group in groups)
776
777
  self.printer.verbose(f'Executed {group_nbr} groups and {total_items} steps')
777
778
 
779
+ def _post_analyze_results_to_dce(self, data_collection_endpoint:str, payload:object):
780
+ """
781
+ Posts the payload to the given (Azure) data collection endpoint
782
+ """
783
+
784
+ #
785
+ # Everything we do here, can be done using native python. And probably this is preferable as well.
786
+ # But I have decided to follow (at least for now) the overall approach of the app and that is
787
+ # executing systems commands.
788
+ # This does require the az cli to be installed, but that is a fair assumption if you are working
789
+ # with terragrunt/terraform and want to post the analyze results to a Data Collection Endpoint.
790
+ # However, not ruling out this will change, but then the change should be transparant.
791
+ #
792
+
793
+ # Get the Azure information
794
+ rc = subprocess.run(
795
+ shlex.split('az account show'),
796
+ check=True,
797
+ stdout=subprocess.PIPE,
798
+ stderr=sys.stderr,
799
+ )
800
+ self.printer.verbose(rc)
801
+
802
+ # Do a few checks
803
+ if rc.returncode != 0:
804
+ raise Exception(f'Could not get Azure account info')
805
+
806
+ # Get the ouptut
807
+ output = json.loads(rc.stdout.decode())
808
+ if output.get('environmentName') != 'AzureCloud':
809
+ raise Exception(f'Environment is not an Azure cloud:\n{json.dumps(output, indent=2)}')
810
+
811
+ tenant_id = output.get('tenantId')
812
+ if not tenant_id:
813
+ raise Exception(f'Could not determine Azure tenant id:\n{json.dumps(output, indent=2)}')
814
+
815
+ principal = output.get('user').get('name')
816
+ if not principal:
817
+ raise Exception(f'Could not determine principal:\n{json.dumps(output, indent=2)}')
818
+
819
+ # TOKEN=$(az account get-access-token --scope "https://monitor.azure.com//.default" | jq -r '.accessToken')
820
+ # Get the Azure OAUTH token
821
+ rc = subprocess.run(
822
+ shlex.split('az account get-access-token --scope "https://monitor.azure.com//.default"'),
823
+ check=True,
824
+ stdout=subprocess.PIPE,
825
+ stderr=sys.stderr,
826
+ )
827
+ self.printer.verbose(rc.returncode) # do not print the token to output
828
+
829
+ # Do a few checks
830
+ if rc.returncode != 0:
831
+ raise Exception(f'Could not get Azure OAUTH token')
832
+
833
+ # Get the ouptut
834
+ output = json.loads(rc.stdout.decode())
835
+ token = output.get('accessToken')
836
+ if not token:
837
+ raise Exception(f'Could not retrieve an access token:\n{json.dumps(output, indent=2)}')
838
+
839
+ # Get the repo info
840
+ rc = subprocess.run(
841
+ shlex.split('git config --get remote.origin.url'),
842
+ check=True,
843
+ stdout=subprocess.PIPE,
844
+ stderr=sys.stderr,
845
+ )
846
+ self.printer.verbose(rc)
847
+
848
+ # Do a few checks
849
+ if rc.returncode != 0:
850
+ raise Exception(f'Could not get git repo info')
851
+
852
+ # Get the ouptut
853
+ repo = rc.stdout.decode().rstrip('\n')
854
+ if not repo:
855
+ raise Exception(f'Could not get git repo info: {repo}')
856
+
857
+ # Get the current path in the repo
858
+ rc = subprocess.run(
859
+ shlex.split('git rev-parse --show-prefix'),
860
+ check=True,
861
+ stdout=subprocess.PIPE,
862
+ stderr=sys.stderr,
863
+ )
864
+ self.printer.verbose(rc)
865
+
866
+ # Do a few checks
867
+ if rc.returncode != 0:
868
+ raise Exception(f'Could not get current scope')
869
+
870
+ # Get the ouptut
871
+ scope = rc.stdout.decode().rstrip('\n')
872
+ if not scope:
873
+ raise Exception(f'Could not get scope: {scope}')
874
+
875
+ # So now we have everything, we can construct the final payload
876
+ dce_payload = [
877
+ {
878
+ "scope": scope,
879
+ "principal": principal,
880
+ "repo": repo,
881
+ "creations": payload.get("summary").get("creations"),
882
+ "updates": payload.get("summary").get("updates"),
883
+ "deletions": payload.get("summary").get("deletions"),
884
+ "minor": payload.get("summary").get("minor"),
885
+ "medium": payload.get("summary").get("medium"),
886
+ "major": payload.get("summary").get("major"),
887
+ "unknown": payload.get("summary").get("unknown"),
888
+ "total": payload.get("summary").get("total"),
889
+ "score": payload.get("summary").get("score"),
890
+ "details": payload.get('details'),
891
+ },
892
+ ]
893
+
894
+ self.printer.verbose('About to log:')
895
+ self.printer.verbose(f'- to: {data_collection_endpoint}')
896
+ self.printer.verbose(f'- payload:\n{json.dumps(dce_payload, indent=2)}')
897
+
898
+ # now do the actual post
899
+ try:
900
+ headers = {
901
+ 'Authorization': f"Bearer {token}",
902
+ 'Content-Type': 'application/json',
903
+ }
904
+ resp = requests.post(
905
+ url=data_collection_endpoint,
906
+ headers=headers,
907
+ json=dce_payload,
908
+ )
909
+
910
+ resp.raise_for_status()
911
+
912
+ except requests.exceptions.RequestException as e:
913
+ # we warn but continue
914
+ self.printer.warning(f'Error while posting the analyze results ({type(e)}): {e}')
915
+ except Exception as e:
916
+ self.printer.error(f'Unexpected error: {e}')
917
+ if self.printer.print_verbose:
918
+ raise(e)
919
+ sys.exit(1)
920
+
921
+ self.printer.verbose('Done')
922
+
778
923
  def run(self, command, debug, dry_run, no_lock, update, upgrade,
779
924
  planfile, auto_approve, clean, working_dir, terragrunt_args):
780
925
  """ Executes a terragrunt command on a single module """
@@ -1001,8 +1146,8 @@ class TgWrap():
1001
1146
  self.printer.verbose(rc)
1002
1147
 
1003
1148
  def analyze(self, exclude_external_dependencies, working_dir, start_at_step,
1004
- out, analyze_config, parallel_execution,
1005
- include_dirs, exclude_dirs, planfile_dir, terragrunt_args):
1149
+ out, analyze_config, parallel_execution, include_dirs, exclude_dirs,
1150
+ planfile_dir, data_collection_endpoint, terragrunt_args):
1006
1151
  """ Analyzes the plan files """
1007
1152
 
1008
1153
  def calculate_score(major: int, medium: int, minor: int) -> float :
@@ -1051,7 +1196,7 @@ class TgWrap():
1051
1196
  config = self.load_yaml_file(analyze_config)
1052
1197
 
1053
1198
  ts_validation_successful = True
1054
- changes = {}
1199
+ details = {}
1055
1200
  drifts = {}
1056
1201
  try:
1057
1202
  # then run it and capture the output
@@ -1093,7 +1238,7 @@ class TgWrap():
1093
1238
  if 'exception' in data:
1094
1239
  raise Exception(data['exception'])
1095
1240
 
1096
- changes[module], ts_success = run_analyze(
1241
+ details[module], ts_success = run_analyze(
1097
1242
  config=config,
1098
1243
  data=data,
1099
1244
  verbose=self.printer.print_verbose,
@@ -1123,7 +1268,7 @@ class TgWrap():
1123
1268
  }
1124
1269
 
1125
1270
  self.printer.header("Analysis results:", print_line_before=True)
1126
- for key, value in changes.items():
1271
+ for key, value in details.items():
1127
1272
  self.printer.header(f'Module: {key}')
1128
1273
  if not value["all"]:
1129
1274
  self.printer.success('No changes detected')
@@ -1147,7 +1292,7 @@ class TgWrap():
1147
1292
  total_drifts["updates"] = total_drifts["updates"] + 1
1148
1293
  self.printer.normal(f'-> {m}')
1149
1294
 
1150
- for key, value in changes.items():
1295
+ for key, value in details.items():
1151
1296
  for type in ["minor", "medium", "major", "unknown", "total"]:
1152
1297
  total_drifts[type] += value["drifts"][type]
1153
1298
 
@@ -1167,24 +1312,31 @@ class TgWrap():
1167
1312
  if total_drifts["unknown"] > 0:
1168
1313
  self.printer.warning(f"For {total_drifts['unknown']} resources, drift score is not configured, please update configuration!")
1169
1314
  self.printer.warning('- Unknowns:')
1170
- for key, value in changes.items():
1315
+ for key, value in details.items():
1171
1316
  for m in value["unknowns"]:
1172
1317
  self.printer.warning(f' -> {m}')
1173
1318
 
1174
- if out:
1319
+ if out or data_collection_endpoint:
1175
1320
  # in the output we convert the dict of dicts to a list of dicts as it makes processing
1176
1321
  # (e.g. by telegraph) easier.
1177
1322
  output = {
1178
- "changes": [],
1323
+ "details": [],
1179
1324
  "summary": {},
1180
1325
  }
1181
- for key, value in changes.items():
1326
+ for key, value in details.items():
1182
1327
  value['module'] = key
1183
- output["changes"].append(value)
1328
+ output["details"].append(value)
1184
1329
 
1185
1330
  output["summary"] = total_drifts
1186
1331
 
1187
- print(json.dumps(output, indent=4))
1332
+ if out:
1333
+ print(json.dumps(output, indent=4))
1334
+
1335
+ if data_collection_endpoint:
1336
+ self._post_analyze_results_to_dce(
1337
+ data_collection_endpoint=data_collection_endpoint,
1338
+ payload=output,
1339
+ )
1188
1340
 
1189
1341
  if not ts_validation_successful:
1190
1342
  self.printer.error("Analysis detected unauthorised deletions, please check your configuration!!!")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tgwrap
3
- Version: 0.8.13
3
+ Version: 0.9.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
@@ -142,6 +142,56 @@ export TGWRAP_PLANFILE_DIR=".terragrunt-cache/current"
142
142
 
143
143
  Or pass it along with the `--planfile-dir|-P` option and it will use that.
144
144
 
145
+ ### Logging the results
146
+
147
+ `tgwrap` supports logging the analyze results to an [Azure Log Analytics](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-overview) custom table.
148
+
149
+ For that, the custom table need to be present, including a [data collection endpoint](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/data-collection-endpoint-overview?tabs=portal) and associated [data collection rule](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/data-collection-rule-overview?tabs=portal).
150
+
151
+ When you want to activate this, just pass `--data-collection-endpoint` (or, more conveniently, set the `TGWRAP_ANALYZE_DATA_COLLECTION_ENDPOINT` environment variable) with the url to which the data can be posted.
152
+
153
+ > Note that for this to work, `tgwrap` assumes that there is a functioning [azure cli](https://learn.microsoft.com/en-us/cli/azure/) available on the system.
154
+
155
+ A payload as below will be posted, and the log analytics table should be able to accomodate for that:
156
+
157
+ ```json
158
+ [
159
+ {
160
+ "scope": "terragrunt/dlzs/data-platform/global/platform/rbac/",
161
+ "principal": "myself",
162
+ "repo": "https://gitlab.com/my-git-repo.git",
163
+ "creations": 0,
164
+ "updates": 0,
165
+ "deletions": 0,
166
+ "minor": 0,
167
+ "medium": 0,
168
+ "major": 0,
169
+ "unknown": 0,
170
+ "total": 0,
171
+ "score": 0.0,
172
+ "details": [
173
+ {
174
+ "drifts": {
175
+ "minor": 0,
176
+ "medium": 0,
177
+ "major": 0,
178
+ "unknown": 0,
179
+ "total": 0,
180
+ "score": 0.0
181
+ },
182
+ "all": [],
183
+ "creations": [],
184
+ "updates": [],
185
+ "deletions": [],
186
+ "unauthorized": [],
187
+ "unknowns": [],
188
+ "module": ""
189
+ }
190
+ ]
191
+ }
192
+ ]
193
+ ```
194
+
145
195
  ## More than a wrapper
146
196
 
147
197
  Over time, tgwrap became more than a wrapper, blantly violating [#1 of the unix philosophy](https://en.wikipedia.org/wiki/Unix_philosophy#:~:text=The%20Unix%20philosophy%20is%20documented,%2C%20as%20yet%20unknown%2C%20program.): 'Make each program do one thing well'.
@@ -0,0 +1,11 @@
1
+ tgwrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ tgwrap/analyze.py,sha256=CsSaGv-be6ATy36z9X7x00gpKY59soJys2VbIzD-tmg,8726
3
+ tgwrap/cli.py,sha256=jP7KuZzqwW2693fVsqEChzIto2T3YyPcSc9kW8ElDWI,29727
4
+ tgwrap/deploy.py,sha256=giLjpN9b4TWov2w8fWyB6jinAUbupMTCUoZHVT442DM,10421
5
+ tgwrap/main.py,sha256=FqpCZ3xGD33YD3IesFMq2QI2AvSv0_4txRpnoPX19Z4,80691
6
+ tgwrap/printer.py,sha256=dkcOCPIPB-IP6pn8QMpa06xlcqPFVaDvxnz-QEpDJV0,2663
7
+ tgwrap-0.9.0.dist-info/LICENSE,sha256=VT-AVxIXt3EQTC-7Hy1uPGnrDNJLqfcgLgJD78fiyx4,1065
8
+ tgwrap-0.9.0.dist-info/METADATA,sha256=IYdY5B0k1OswfcfVCbdwFOHs_itQXKq9fgbtjZPusIw,13334
9
+ tgwrap-0.9.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
10
+ tgwrap-0.9.0.dist-info/entry_points.txt,sha256=H8X0PMPmd4aW7Y9iyChZ0Ug6RWGXqhRUvHH-6f6Mxz0,42
11
+ tgwrap-0.9.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- tgwrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- tgwrap/analyze.py,sha256=CsSaGv-be6ATy36z9X7x00gpKY59soJys2VbIzD-tmg,8726
3
- tgwrap/cli.py,sha256=sWUHP4EYUeKpYP3KgU3t2Er4YLlnwfXNVQkgVwmGFPM,29342
4
- tgwrap/deploy.py,sha256=bJiox_fz8JsoPreX4woW6-EqAebhpJWnKUVLVeGXkrI,10000
5
- tgwrap/main.py,sha256=Dqqzqe2x7UXHgAgQL764CfoZ6V0JFbk4TphBkBcYcC8,74872
6
- tgwrap/printer.py,sha256=dkcOCPIPB-IP6pn8QMpa06xlcqPFVaDvxnz-QEpDJV0,2663
7
- tgwrap-0.8.13.dist-info/LICENSE,sha256=VT-AVxIXt3EQTC-7Hy1uPGnrDNJLqfcgLgJD78fiyx4,1065
8
- tgwrap-0.8.13.dist-info/METADATA,sha256=EDgJtU6kePAZrn-aft9LWPDImK_5qEYaBoJCGhY_L3s,11616
9
- tgwrap-0.8.13.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
10
- tgwrap-0.8.13.dist-info/entry_points.txt,sha256=H8X0PMPmd4aW7Y9iyChZ0Ug6RWGXqhRUvHH-6f6Mxz0,42
11
- tgwrap-0.8.13.dist-info/RECORD,,