tgwrap 0.8.14__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 +8 -2
- tgwrap/deploy.py +8 -1
- tgwrap/main.py +165 -13
- {tgwrap-0.8.14.dist-info → tgwrap-0.9.0.dist-info}/METADATA +51 -1
- tgwrap-0.9.0.dist-info/RECORD +11 -0
- tgwrap-0.8.14.dist-info/RECORD +0 -11
- {tgwrap-0.8.14.dist-info → tgwrap-0.9.0.dist-info}/LICENSE +0 -0
- {tgwrap-0.8.14.dist-info → tgwrap-0.9.0.dist-info}/WHEEL +0 -0
- {tgwrap-0.8.14.dist-info → tgwrap-0.9.0.dist-info}/entry_points.txt +0 -0
tgwrap/cli.py
CHANGED
@@ -403,11 +403,16 @@ def run_import(address, id, verbose, dry_run, working_dir, no_lock, terragrunt_a
|
|
403
403
|
envvar='TGWRAP_PLANFILE_DIR', type=click.Path(),
|
404
404
|
show_default=True,
|
405
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
|
+
)
|
406
411
|
@click.argument('terragrunt-args', nargs=-1, type=click.UNPROCESSED)
|
407
412
|
@click.version_option(version=__version__)
|
408
413
|
def run_analyze(verbose, exclude_external_dependencies, working_dir, start_at_step,
|
409
|
-
out, analyze_config, parallel_execution,
|
410
|
-
|
414
|
+
out, analyze_config, parallel_execution, include_dir, exclude_dir,
|
415
|
+
planfile_dir, data_collection_endpoint, terragrunt_args):
|
411
416
|
""" Analyzes the plan files """
|
412
417
|
|
413
418
|
check_latest_version(verbose)
|
@@ -423,6 +428,7 @@ def run_analyze(verbose, exclude_external_dependencies, working_dir, start_at_st
|
|
423
428
|
include_dirs=include_dir,
|
424
429
|
exclude_dirs=exclude_dir,
|
425
430
|
planfile_dir=planfile_dir,
|
431
|
+
data_collection_endpoint=data_collection_endpoint,
|
426
432
|
terragrunt_args=terragrunt_args,
|
427
433
|
)
|
428
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
-
"
|
1323
|
+
"details": [],
|
1179
1324
|
"summary": {},
|
1180
1325
|
}
|
1181
|
-
for key, value in
|
1326
|
+
for key, value in details.items():
|
1182
1327
|
value['module'] = key
|
1183
|
-
output["
|
1328
|
+
output["details"].append(value)
|
1184
1329
|
|
1185
1330
|
output["summary"] = total_drifts
|
1186
1331
|
|
1187
|
-
|
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.
|
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,,
|
tgwrap-0.8.14.dist-info/RECORD
DELETED
@@ -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=ZOt_aEGhfKGC81FhC5PNF8qhEuc5apyQC3ipQOonjco,29388
|
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.14.dist-info/LICENSE,sha256=VT-AVxIXt3EQTC-7Hy1uPGnrDNJLqfcgLgJD78fiyx4,1065
|
8
|
-
tgwrap-0.8.14.dist-info/METADATA,sha256=Z-7SMM8jMi00xbW-PRAiF8ZGuoyER3S_h_YEoYLK4M4,11616
|
9
|
-
tgwrap-0.8.14.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
10
|
-
tgwrap-0.8.14.dist-info/entry_points.txt,sha256=H8X0PMPmd4aW7Y9iyChZ0Ug6RWGXqhRUvHH-6f6Mxz0,42
|
11
|
-
tgwrap-0.8.14.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|