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.
@@ -1,22 +1,22 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: tgwrap
3
- Version: 0.8.12
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.8,<4.0
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.8.12"
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.8"
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} 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"]
@@ -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"\[(.+?)\]", "[[]\g<1>[]]", pattern)
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"\[(.+?)\]", "[[]\g<1>[]]", pattern)
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