tgwrap 0.8.12__tar.gz → 0.11.3__tar.gz
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-0.8.12 → tgwrap-0.11.3}/PKG-INFO +165 -8
- {tgwrap-0.8.12 → tgwrap-0.11.3}/README.md +156 -0
- {tgwrap-0.8.12 → tgwrap-0.11.3}/pyproject.toml +6 -2
- {tgwrap-0.8.12 → tgwrap-0.11.3}/tgwrap/analyze.py +62 -18
- {tgwrap-0.8.12 → tgwrap-0.11.3}/tgwrap/cli.py +117 -25
- {tgwrap-0.8.12 → tgwrap-0.11.3}/tgwrap/deploy.py +10 -3
- tgwrap-0.11.3/tgwrap/inspector-resources-template.yml +63 -0
- tgwrap-0.11.3/tgwrap/inspector.py +438 -0
- {tgwrap-0.8.12 → tgwrap-0.11.3}/tgwrap/main.py +583 -126
- {tgwrap-0.8.12 → tgwrap-0.11.3}/tgwrap/printer.py +3 -0
- {tgwrap-0.8.12 → tgwrap-0.11.3}/LICENSE +0 -0
- {tgwrap-0.8.12 → tgwrap-0.11.3}/tgwrap/__init__.py +0 -0
@@ -1,22 +1,22 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: tgwrap
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.11.3
|
4
4
|
Summary: A (terragrunt) wrapper around a (terraform) wrapper around ....
|
5
|
-
Home-page: https://gitlab.com/lunadata/tgwrap
|
6
5
|
License: MIT
|
7
6
|
Keywords: terraform,terragrunt,terrasafe,python
|
8
7
|
Author: Gerco Grandia
|
9
8
|
Author-email: gerco.grandia@4synergy.nl
|
10
|
-
Requires-Python: >=3.
|
9
|
+
Requires-Python: >=3.12,<4.0
|
11
10
|
Classifier: License :: OSI Approved :: MIT License
|
12
11
|
Classifier: Programming Language :: Python :: 3
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
17
12
|
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
14
|
+
Requires-Dist: azure-core (>=1.30.2,<2.0.0)
|
15
|
+
Requires-Dist: azure-identity (>=1.17.1,<2.0.0)
|
16
|
+
Requires-Dist: azure-mgmt-authorization (>=4.0.0,<5.0.0)
|
18
17
|
Requires-Dist: click (>=8.0)
|
19
18
|
Requires-Dist: inquirer (>=3.1.4,<4.0.0)
|
19
|
+
Requires-Dist: jinja2 (>=3.1.4,<4.0.0)
|
20
20
|
Requires-Dist: networkx (>=2.8.8,<3.0.0)
|
21
21
|
Requires-Dist: outdated (>=0.2.2)
|
22
22
|
Requires-Dist: pydot (>=1.4.2,<2.0.0)
|
@@ -25,6 +25,7 @@ Requires-Dist: python-hcl2 (>=4.3.2,<5.0.0)
|
|
25
25
|
Requires-Dist: pyyaml (>=6.0)
|
26
26
|
Requires-Dist: terrasafe (>=0.5.1,<0.6.0)
|
27
27
|
Project-URL: Documentation, https://gitlab.com/lunadata/tgwrap/
|
28
|
+
Project-URL: Homepage, https://gitlab.com/lunadata/tgwrap
|
28
29
|
Project-URL: Repository, https://gitlab.com/lunadata/tgwrap
|
29
30
|
Description-Content-Type: text/markdown
|
30
31
|
|
@@ -72,6 +73,8 @@ And this was not something a bunch of aliases could solve, hence this wrapper wa
|
|
72
73
|
|
73
74
|
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
75
|
|
76
|
+
> you can ignore minor changes, such as tag updates, with `tgwrap analyze -i tags`
|
77
|
+
|
75
78
|
It needs a config file as follows:
|
76
79
|
|
77
80
|
```yaml
|
@@ -142,6 +145,75 @@ export TGWRAP_PLANFILE_DIR=".terragrunt-cache/current"
|
|
142
145
|
|
143
146
|
Or pass it along with the `--planfile-dir|-P` option and it will use that.
|
144
147
|
|
148
|
+
### Logging the results
|
149
|
+
|
150
|
+
`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.
|
151
|
+
|
152
|
+
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).
|
153
|
+
|
154
|
+
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.
|
155
|
+
|
156
|
+
> 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.
|
157
|
+
|
158
|
+
A payload as below will be posted, and the log analytics table should be able to accomodate for that:
|
159
|
+
|
160
|
+
```json
|
161
|
+
[
|
162
|
+
{
|
163
|
+
"scope": "terragrunt/dlzs/data-platform/global/platform/rbac/",
|
164
|
+
"principal": "myself",
|
165
|
+
"repo": "https://gitlab.com/my-git-repo.git",
|
166
|
+
"creations": 0,
|
167
|
+
"updates": 0,
|
168
|
+
"deletions": 0,
|
169
|
+
"minor": 0,
|
170
|
+
"medium": 0,
|
171
|
+
"major": 0,
|
172
|
+
"unknown": 0,
|
173
|
+
"total": 0,
|
174
|
+
"score": 0.0,
|
175
|
+
"details": [
|
176
|
+
{
|
177
|
+
"drifts": {
|
178
|
+
"minor": 0,
|
179
|
+
"medium": 0,
|
180
|
+
"major": 0,
|
181
|
+
"unknown": 0,
|
182
|
+
"total": 0,
|
183
|
+
"score": 0.0
|
184
|
+
},
|
185
|
+
"all": [],
|
186
|
+
"creations": [],
|
187
|
+
"updates": [],
|
188
|
+
"deletions": [],
|
189
|
+
"unauthorized": [],
|
190
|
+
"unknowns": [],
|
191
|
+
"module": ""
|
192
|
+
}
|
193
|
+
]
|
194
|
+
}
|
195
|
+
]
|
196
|
+
```
|
197
|
+
|
198
|
+
The log analytics (custom) table should have a schema that is able to cope with the message above:
|
199
|
+
|
200
|
+
| Field | Type |
|
201
|
+
|----------------|----------|
|
202
|
+
| creations | Int |
|
203
|
+
| deletions | Int |
|
204
|
+
| details | Dynamic |
|
205
|
+
| major | Int |
|
206
|
+
| medium | Int |
|
207
|
+
| minor | Int |
|
208
|
+
| principal | String |
|
209
|
+
| repo | String |
|
210
|
+
| scope | String |
|
211
|
+
| score | Int |
|
212
|
+
| TimeGenerated | Datetime |
|
213
|
+
| total | Int |
|
214
|
+
| unknown | Int |
|
215
|
+
| updates | Int |
|
216
|
+
|
145
217
|
## More than a wrapper
|
146
218
|
|
147
219
|
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'.
|
@@ -216,6 +288,7 @@ deploy: # which modules do you want to deploy
|
|
216
288
|
source_stage: dev
|
217
289
|
source_dir: platform # optional, if the source modules are not directly in the stage dir, but in <stage>/<source_dir> directory
|
218
290
|
base_dir: platform # optional, if you want to deploy the base modules in its own dir, side by side with substacks
|
291
|
+
include_global_config_files: false # optional, overrides the CLI input
|
219
292
|
config_dir: ../../../config # this is relative to where you run the deploy
|
220
293
|
configs:
|
221
294
|
- my-config.hcl
|
@@ -257,6 +330,90 @@ global_config_files:
|
|
257
330
|
source: ../../terrasafe-config.json
|
258
331
|
```
|
259
332
|
|
333
|
+
## Inspecting deployed infrastructure
|
334
|
+
|
335
|
+
Testing infra-as-code is hard, even though test frameworks are becoming more common these days. But the standard test approaches typically work with temporary infrastructures, while it is often also useful to test a deployed infrastructure.
|
336
|
+
|
337
|
+
Frameworks like [Chef's InSpec](https://docs.chef.io/inspec/) aims at solving that, but it is pretty config management heavy (but there are add-ons for aws and azure infra). It has a steep learning curve, we only need a tiny part of it, and also comes with a commercial license.
|
338
|
+
|
339
|
+
For what we need ('is infra deployed and are the main role assignments still in place') it was pretty easy to implement in python.
|
340
|
+
|
341
|
+
For this, you can now run the `inspect` command, which will then inspect real infrastructure and role assignments, and report back whether it meets the expectations (as declared in a config file):
|
342
|
+
|
343
|
+
```yaml
|
344
|
+
---
|
345
|
+
location:
|
346
|
+
code: westeurope
|
347
|
+
full: West Europe
|
348
|
+
|
349
|
+
# the entra id groups ar specified as a map as these will be checked for existence
|
350
|
+
# but also used for role assignment validation
|
351
|
+
entra_id_groups:
|
352
|
+
platform_admins: '{domain}-platform-admins'
|
353
|
+
cost_admins: '{domain}-cost-admins'
|
354
|
+
data_admins: '{domain}-data-admins'
|
355
|
+
just_testing: group-does-not-exist
|
356
|
+
|
357
|
+
# the resources to check
|
358
|
+
resources:
|
359
|
+
- identifier: 'kv-{domain}-euw-{stage}-base'
|
360
|
+
# due to length limitations in resource names, some shortening in the name might have taken place
|
361
|
+
# so you can provide alternative ids
|
362
|
+
alternative_ids:
|
363
|
+
- 'kv-{domain}-euw-{stage}-bs'
|
364
|
+
- 'kv{domain}euw{stage}bs'
|
365
|
+
- 'kv{domain}euw{stage}base'
|
366
|
+
type: key_vault
|
367
|
+
resource_group: 'rg-{domain}-euw-{stage}-base'
|
368
|
+
role_assignments:
|
369
|
+
- platform_admins: Owner
|
370
|
+
- platform_admins: Key Vault Secrets Officer
|
371
|
+
- data_admins: Key Vault Secrets Officer
|
372
|
+
```
|
373
|
+
|
374
|
+
After which you can run the following:
|
375
|
+
|
376
|
+
```console
|
377
|
+
tgwrap inspect -d domain -s sbx -a 886d4e58-a178-4c50-ae65-xxxxxxxxxx -c ./inspect-config.yml
|
378
|
+
......
|
379
|
+
|
380
|
+
Inspection status:
|
381
|
+
entra_id_group: dps-platform-admins
|
382
|
+
-> Resource: OK (Resource dps-platform-admins of type entra_id_group OK)
|
383
|
+
entra_id_group: dps-cost-admins
|
384
|
+
-> Resource: OK (Resource dps-cost-admins of type entra_id_group OK)
|
385
|
+
entra_id_group: dps-data-admins
|
386
|
+
-> Resource: OK (Resource dps-data-admins of type entra_id_group OK)
|
387
|
+
entra_id_group: group-does-not-exist
|
388
|
+
-> Resource: NEX (Resource group-does-not-exist of type entra_id_group not found)
|
389
|
+
key_vault: kv-dps-euw-sbx-base
|
390
|
+
-> Resource: OK (Resource kv-dps-euw-sbx-base of type key_vault OK)
|
391
|
+
-> RBAC: NOK (Principal platform_admins has NOT role Owner assigned; )
|
392
|
+
subscription: 886d4e58-a178-4c50-ae65-xxxxxxxxxx
|
393
|
+
-> Resource: OK (Resource 886d4e58-a178-4c50-ae65-xxxxxxxxxx of type subscription OK)
|
394
|
+
-> RBAC: NC (Role assignments not checked)
|
395
|
+
```
|
396
|
+
|
397
|
+
You can sent the results also to a data collection endpoint (seel also [Logging the results](#logging-the-results)).
|
398
|
+
|
399
|
+
For that, a custom table should exist with the following structure:
|
400
|
+
|
401
|
+
|
402
|
+
| Field | Type |
|
403
|
+
|----------------------------|----------|
|
404
|
+
| domain | String |
|
405
|
+
| substack | String |
|
406
|
+
| stage | String |
|
407
|
+
| subscription_id | String |
|
408
|
+
| resource_type | String |
|
409
|
+
| inspect_status_code | String |
|
410
|
+
| inspect_status | String |
|
411
|
+
| inspect_message | String |
|
412
|
+
| rbac_assignment_status_code| String |
|
413
|
+
| rbac_assignment_status | String |
|
414
|
+
| rbac_assignment_message | String |
|
415
|
+
| resource | String |
|
416
|
+
|
260
417
|
## Generating change logs
|
261
418
|
|
262
419
|
tgwrap can generate a change log by running:
|
@@ -42,6 +42,8 @@ And this was not something a bunch of aliases could solve, hence this wrapper wa
|
|
42
42
|
|
43
43
|
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.
|
44
44
|
|
45
|
+
> you can ignore minor changes, such as tag updates, with `tgwrap analyze -i tags`
|
46
|
+
|
45
47
|
It needs a config file as follows:
|
46
48
|
|
47
49
|
```yaml
|
@@ -112,6 +114,75 @@ export TGWRAP_PLANFILE_DIR=".terragrunt-cache/current"
|
|
112
114
|
|
113
115
|
Or pass it along with the `--planfile-dir|-P` option and it will use that.
|
114
116
|
|
117
|
+
### Logging the results
|
118
|
+
|
119
|
+
`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.
|
120
|
+
|
121
|
+
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).
|
122
|
+
|
123
|
+
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.
|
124
|
+
|
125
|
+
> 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.
|
126
|
+
|
127
|
+
A payload as below will be posted, and the log analytics table should be able to accomodate for that:
|
128
|
+
|
129
|
+
```json
|
130
|
+
[
|
131
|
+
{
|
132
|
+
"scope": "terragrunt/dlzs/data-platform/global/platform/rbac/",
|
133
|
+
"principal": "myself",
|
134
|
+
"repo": "https://gitlab.com/my-git-repo.git",
|
135
|
+
"creations": 0,
|
136
|
+
"updates": 0,
|
137
|
+
"deletions": 0,
|
138
|
+
"minor": 0,
|
139
|
+
"medium": 0,
|
140
|
+
"major": 0,
|
141
|
+
"unknown": 0,
|
142
|
+
"total": 0,
|
143
|
+
"score": 0.0,
|
144
|
+
"details": [
|
145
|
+
{
|
146
|
+
"drifts": {
|
147
|
+
"minor": 0,
|
148
|
+
"medium": 0,
|
149
|
+
"major": 0,
|
150
|
+
"unknown": 0,
|
151
|
+
"total": 0,
|
152
|
+
"score": 0.0
|
153
|
+
},
|
154
|
+
"all": [],
|
155
|
+
"creations": [],
|
156
|
+
"updates": [],
|
157
|
+
"deletions": [],
|
158
|
+
"unauthorized": [],
|
159
|
+
"unknowns": [],
|
160
|
+
"module": ""
|
161
|
+
}
|
162
|
+
]
|
163
|
+
}
|
164
|
+
]
|
165
|
+
```
|
166
|
+
|
167
|
+
The log analytics (custom) table should have a schema that is able to cope with the message above:
|
168
|
+
|
169
|
+
| Field | Type |
|
170
|
+
|----------------|----------|
|
171
|
+
| creations | Int |
|
172
|
+
| deletions | Int |
|
173
|
+
| details | Dynamic |
|
174
|
+
| major | Int |
|
175
|
+
| medium | Int |
|
176
|
+
| minor | Int |
|
177
|
+
| principal | String |
|
178
|
+
| repo | String |
|
179
|
+
| scope | String |
|
180
|
+
| score | Int |
|
181
|
+
| TimeGenerated | Datetime |
|
182
|
+
| total | Int |
|
183
|
+
| unknown | Int |
|
184
|
+
| updates | Int |
|
185
|
+
|
115
186
|
## More than a wrapper
|
116
187
|
|
117
188
|
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'.
|
@@ -186,6 +257,7 @@ deploy: # which modules do you want to deploy
|
|
186
257
|
source_stage: dev
|
187
258
|
source_dir: platform # optional, if the source modules are not directly in the stage dir, but in <stage>/<source_dir> directory
|
188
259
|
base_dir: platform # optional, if you want to deploy the base modules in its own dir, side by side with substacks
|
260
|
+
include_global_config_files: false # optional, overrides the CLI input
|
189
261
|
config_dir: ../../../config # this is relative to where you run the deploy
|
190
262
|
configs:
|
191
263
|
- my-config.hcl
|
@@ -227,6 +299,90 @@ global_config_files:
|
|
227
299
|
source: ../../terrasafe-config.json
|
228
300
|
```
|
229
301
|
|
302
|
+
## Inspecting deployed infrastructure
|
303
|
+
|
304
|
+
Testing infra-as-code is hard, even though test frameworks are becoming more common these days. But the standard test approaches typically work with temporary infrastructures, while it is often also useful to test a deployed infrastructure.
|
305
|
+
|
306
|
+
Frameworks like [Chef's InSpec](https://docs.chef.io/inspec/) aims at solving that, but it is pretty config management heavy (but there are add-ons for aws and azure infra). It has a steep learning curve, we only need a tiny part of it, and also comes with a commercial license.
|
307
|
+
|
308
|
+
For what we need ('is infra deployed and are the main role assignments still in place') it was pretty easy to implement in python.
|
309
|
+
|
310
|
+
For this, you can now run the `inspect` command, which will then inspect real infrastructure and role assignments, and report back whether it meets the expectations (as declared in a config file):
|
311
|
+
|
312
|
+
```yaml
|
313
|
+
---
|
314
|
+
location:
|
315
|
+
code: westeurope
|
316
|
+
full: West Europe
|
317
|
+
|
318
|
+
# the entra id groups ar specified as a map as these will be checked for existence
|
319
|
+
# but also used for role assignment validation
|
320
|
+
entra_id_groups:
|
321
|
+
platform_admins: '{domain}-platform-admins'
|
322
|
+
cost_admins: '{domain}-cost-admins'
|
323
|
+
data_admins: '{domain}-data-admins'
|
324
|
+
just_testing: group-does-not-exist
|
325
|
+
|
326
|
+
# the resources to check
|
327
|
+
resources:
|
328
|
+
- identifier: 'kv-{domain}-euw-{stage}-base'
|
329
|
+
# due to length limitations in resource names, some shortening in the name might have taken place
|
330
|
+
# so you can provide alternative ids
|
331
|
+
alternative_ids:
|
332
|
+
- 'kv-{domain}-euw-{stage}-bs'
|
333
|
+
- 'kv{domain}euw{stage}bs'
|
334
|
+
- 'kv{domain}euw{stage}base'
|
335
|
+
type: key_vault
|
336
|
+
resource_group: 'rg-{domain}-euw-{stage}-base'
|
337
|
+
role_assignments:
|
338
|
+
- platform_admins: Owner
|
339
|
+
- platform_admins: Key Vault Secrets Officer
|
340
|
+
- data_admins: Key Vault Secrets Officer
|
341
|
+
```
|
342
|
+
|
343
|
+
After which you can run the following:
|
344
|
+
|
345
|
+
```console
|
346
|
+
tgwrap inspect -d domain -s sbx -a 886d4e58-a178-4c50-ae65-xxxxxxxxxx -c ./inspect-config.yml
|
347
|
+
......
|
348
|
+
|
349
|
+
Inspection status:
|
350
|
+
entra_id_group: dps-platform-admins
|
351
|
+
-> Resource: OK (Resource dps-platform-admins of type entra_id_group OK)
|
352
|
+
entra_id_group: dps-cost-admins
|
353
|
+
-> Resource: OK (Resource dps-cost-admins of type entra_id_group OK)
|
354
|
+
entra_id_group: dps-data-admins
|
355
|
+
-> Resource: OK (Resource dps-data-admins of type entra_id_group OK)
|
356
|
+
entra_id_group: group-does-not-exist
|
357
|
+
-> Resource: NEX (Resource group-does-not-exist of type entra_id_group not found)
|
358
|
+
key_vault: kv-dps-euw-sbx-base
|
359
|
+
-> Resource: OK (Resource kv-dps-euw-sbx-base of type key_vault OK)
|
360
|
+
-> RBAC: NOK (Principal platform_admins has NOT role Owner assigned; )
|
361
|
+
subscription: 886d4e58-a178-4c50-ae65-xxxxxxxxxx
|
362
|
+
-> Resource: OK (Resource 886d4e58-a178-4c50-ae65-xxxxxxxxxx of type subscription OK)
|
363
|
+
-> RBAC: NC (Role assignments not checked)
|
364
|
+
```
|
365
|
+
|
366
|
+
You can sent the results also to a data collection endpoint (seel also [Logging the results](#logging-the-results)).
|
367
|
+
|
368
|
+
For that, a custom table should exist with the following structure:
|
369
|
+
|
370
|
+
|
371
|
+
| Field | Type |
|
372
|
+
|----------------------------|----------|
|
373
|
+
| domain | String |
|
374
|
+
| substack | String |
|
375
|
+
| stage | String |
|
376
|
+
| subscription_id | String |
|
377
|
+
| resource_type | String |
|
378
|
+
| inspect_status_code | String |
|
379
|
+
| inspect_status | String |
|
380
|
+
| inspect_message | String |
|
381
|
+
| rbac_assignment_status_code| String |
|
382
|
+
| rbac_assignment_status | String |
|
383
|
+
| rbac_assignment_message | String |
|
384
|
+
| resource | String |
|
385
|
+
|
230
386
|
## Generating change logs
|
231
387
|
|
232
388
|
tgwrap can generate a change log by running:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "tgwrap"
|
3
|
-
version = "0.
|
3
|
+
version = "0.11.3"
|
4
4
|
description = "A (terragrunt) wrapper around a (terraform) wrapper around ...."
|
5
5
|
authors = ["Gerco Grandia <gerco.grandia@4synergy.nl>", "Pascal Alma <pascal.alma@4synergy.nl>"]
|
6
6
|
license = "MIT"
|
@@ -12,7 +12,7 @@ keywords = ["terraform", "terragrunt", "terrasafe", "python"]
|
|
12
12
|
include = ["tgwrap", "tgwrap/lib"]
|
13
13
|
|
14
14
|
[tool.poetry.dependencies]
|
15
|
-
python = "^3.
|
15
|
+
python = "^3.12"
|
16
16
|
pydot = "^1.4.2"
|
17
17
|
networkx = "^2.8.8"
|
18
18
|
click = ">= 8.0"
|
@@ -22,6 +22,10 @@ pyhcl = "^0.4.4"
|
|
22
22
|
pyyaml = ">= 6.0"
|
23
23
|
python-hcl2 = "^4.3.2"
|
24
24
|
inquirer = "^3.1.4"
|
25
|
+
azure-identity = "^1.17.1"
|
26
|
+
azure-core = "^1.30.2"
|
27
|
+
azure-mgmt-authorization = "^4.0.0"
|
28
|
+
jinja2 = "^3.1.4"
|
25
29
|
|
26
30
|
[tool.poetry.scripts]
|
27
31
|
tgwrap = "tgwrap.cli:main"
|
@@ -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)
|
@@ -34,6 +34,7 @@ def run_analyze(config, data, verbose=False):
|
|
34
34
|
|
35
35
|
detected_changes = get_resource_actions(data)
|
36
36
|
|
37
|
+
ignorable_updates = []
|
37
38
|
if config:
|
38
39
|
ts_default_levels = {
|
39
40
|
'low': 'ignore_deletion',
|
@@ -44,11 +45,28 @@ def run_analyze(config, data, verbose=False):
|
|
44
45
|
for criticallity, ts_level in ts_default_levels.items():
|
45
46
|
ts_config[ts_level] = [f"*{key}*" for key, value in config[criticallity].items() if value.get('terrasafe_level', ts_level) == ts_level]
|
46
47
|
|
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"]
|
@@ -185,7 +229,7 @@ def has_update_action(resource):
|
|
185
229
|
|
186
230
|
def is_resource_match_any(resource_address, pattern_list):
|
187
231
|
for pattern in pattern_list:
|
188
|
-
pattern = re.sub(r"\[(.+?)\]", "[[]
|
232
|
+
pattern = re.sub(r"\[(.+?)\]", "[[]\\g<1>[]]", pattern)
|
189
233
|
if fnmatch.fnmatch(resource_address, pattern):
|
190
234
|
return True
|
191
235
|
return False
|
@@ -193,7 +237,7 @@ def is_resource_match_any(resource_address, pattern_list):
|
|
193
237
|
|
194
238
|
def get_matching_dd_config(resource_address, dd_config):
|
195
239
|
for pattern, config in dd_config.items():
|
196
|
-
pattern = re.sub(r"\[(.+?)\]", "[[]
|
240
|
+
pattern = re.sub(r"\[(.+?)\]", "[[]\\g<1>[]]", pattern)
|
197
241
|
if fnmatch.fnmatch(resource_address, pattern):
|
198
242
|
return True, config
|
199
243
|
return False, None
|