qontract-reconcile 0.10.1rc216__py3-none-any.whl → 0.10.1rc218__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc216
3
+ Version: 0.10.1rc218
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -23,12 +23,12 @@ reconcile/github_repo_invites.py,sha256=n5_gUcGotBBnvx3F41auCqX_gbZKut_XRTWGlsrK
23
23
  reconcile/github_repo_permissions_validator.py,sha256=dcbXdUx6imjNchjp3pg9-z1i7lFEGOr_28GvsiwO5Xw,1734
24
24
  reconcile/github_users.py,sha256=nfTq78QRONIfDVj-5O3bD6psllJjzWFnog-EJ1WqFPU,3672
25
25
  reconcile/github_validator.py,sha256=cVTVxJIGR4a1Jz8wrdXEAb_CMpXUzvykVmUURX4cook,917
26
- reconcile/gitlab_fork_compliance.py,sha256=XEwm5tlTvoY95py4MsBHgGcw6_8tWtJ9hLyUKa6y2_k,4246
27
- reconcile/gitlab_housekeeping.py,sha256=UesYfhhQMzXkL9oWTr2yh8r0wZKIEe9PshTAYTXsqk4,22169
26
+ reconcile/gitlab_fork_compliance.py,sha256=nLrwtoj5bTlaMutpKUc4R6lqZtpqB0otcIOMNfIxUsU,4223
27
+ reconcile/gitlab_housekeeping.py,sha256=9rZZ97R0eMHW67YksHM2ATmHLQoq62bmimfUSeI_M0Q,21276
28
28
  reconcile/gitlab_labeler.py,sha256=avNifNROyGhko6WDAaTH3ixA86qzxdBsSk1IisRoxao,4635
29
29
  reconcile/gitlab_members.py,sha256=M6LwFOrwgvl1NNdOJa1mrQFUon-bEVv1AyhGeLed454,8443
30
30
  reconcile/gitlab_mr_sqs_consumer.py,sha256=O46mdziPgGOndbU-0_UJKJVUaiEoVzJPEgKm4_UvYoI,2571
31
- reconcile/gitlab_owners.py,sha256=3OwGjPsKzKBtp0l2wWfh1RwnWYDeLejykCq5SHof0vk,13922
31
+ reconcile/gitlab_owners.py,sha256=n5_pX841OST3dGfMUEXtqIcyqV4NjzSEvh2E0up7Y1c,13876
32
32
  reconcile/gitlab_permissions.py,sha256=ciEKj_wnRbS_vs_ZwcUeD6HkWVe3osAuotFqJSmvd94,1638
33
33
  reconcile/gitlab_projects.py,sha256=K3tFf_aD1W4Ijp5q-9Qek3kwFGEWPcZ1kd7tzFJ4GyQ,1781
34
34
  reconcile/integrations_manager.py,sha256=zUCh1bYrnNoT_6SSQO-yYA2QdDxfCuzwb1tjcByIOaE,8885
@@ -126,7 +126,7 @@ reconcile/aws_ami_cleanup/integration.py,sha256=E--71oG3HhrUgBieAf51wNE1N1xtsuOa
126
126
  reconcile/change_owners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
127
  reconcile/change_owners/approver.py,sha256=L7XJWJ-rgn8BOmeMb6lBDV8lHFCUaNoHGDSD7OH03vA,2244
128
128
  reconcile/change_owners/bundle.py,sha256=Bh9v08tIw8TieG4EE9kVI9iFN1sW7Z6Zyb8DTbsv6F0,5371
129
- reconcile/change_owners/change_owners.py,sha256=L3Glirkd5a6TnPyseANq8THhnV6oJP_2PcqAfxhkjxE,13257
129
+ reconcile/change_owners/change_owners.py,sha256=2Z182TKDpcvPwLtyTDbHdA11IMXuWrBSQxUoeUmdXbU,13268
130
130
  reconcile/change_owners/change_types.py,sha256=3UFjyuTW0-si9pvaKfqGCWGwTiBVi7n9HZnfHk_x5iY,31486
131
131
  reconcile/change_owners/changes.py,sha256=KrtaDTM_jxyYsHPUEjJCWJr186L4a-NsxCnztdSUry4,16912
132
132
  reconcile/change_owners/decision.py,sha256=JBMhNG8UqaKzTZgaKQLTPn8KW8CSEvq8clP76ZfQT6s,6381
@@ -330,7 +330,7 @@ reconcile/test/test_closedbox_endpoint_monitoring.py,sha256=isMHYwRWMFARU2nbJgbl
330
330
  reconcile/test/test_gabi_authorized_users.py,sha256=vyO9-1w6OtGQJvJuTazJMVAjQbUd90sgsHcMyIYkUC8,2483
331
331
  reconcile/test/test_github_org.py,sha256=j3KeB4OnSln1gm2hidce49xdMru-j75NS3cM-AEgzZc,4511
332
332
  reconcile/test/test_github_repo_invites.py,sha256=QJ0VFk5B59rx4XtHoT6XOGWw9xRIZMen_cgtviN_Vi8,3419
333
- reconcile/test/test_gitlab_housekeeping.py,sha256=ScFp3ztym2Q4lJwuTclq9HIHaT8JAymGoP1t-ynRFCQ,10049
333
+ reconcile/test/test_gitlab_housekeeping.py,sha256=7EpWikaWJH52IlA2PZs7vz4GEgID-_dfZBpJNlwhMSw,10018
334
334
  reconcile/test/test_gitlab_labeler.py,sha256=vFLUJXSIaCduj6wSqgw7Fg7FhlopaDnYI5SLzNHtLoY,4362
335
335
  reconcile/test/test_gitlab_members.py,sha256=dP_dm-1THba9Vyzcq-EX1tdmBoX2hq8R-MY4Uqknq5s,9896
336
336
  reconcile/test/test_instrumented_wrappers.py,sha256=CZzhnQH0c4i7-Rxjg7-0dfFMvVPegLHL46z5NHOOCwo,608
@@ -464,7 +464,7 @@ reconcile/utils/filtering.py,sha256=dw7Ok7HXjZb0ruvCWHFh194rtunX1COLDTRnNfOpwQU,
464
464
  reconcile/utils/git.py,sha256=kgjN93MMB5mnkuNb1n53f5kldGGf5u0pBHj9YJbiE_c,1455
465
465
  reconcile/utils/git_secrets.py,sha256=897nRs7tycA3m7YYeVEbzOhI8RFrI9IJT2E0di1eJhc,1956
466
466
  reconcile/utils/github_api.py,sha256=6gdlKK0W3vZpxbbtOcohRgvZ4YkiSki7Gxdb16goHPo,2316
467
- reconcile/utils/gitlab_api.py,sha256=6udOgddf0qcTSgJTJcSHL5YJrUKiAFnTg7ZrHXKbhso,24939
467
+ reconcile/utils/gitlab_api.py,sha256=v_cf2xt7trUlYFRYsuJ9ZkCEtFiO_0coAjlxBjjHedc,24839
468
468
  reconcile/utils/gpg.py,sha256=EKG7_fdMv8BMlV5yUdPiqoTx-KrzmVSEAl2sLkaKwWI,1123
469
469
  reconcile/utils/gql.py,sha256=KogegLFsvjiTWqPBDSb4qJToISrdsDeLJ3gkHwi1DQ8,11672
470
470
  reconcile/utils/helm.py,sha256=IWlB_LrBK6ydwNQuZP2aMJGrtQw0lW7qdFqSrd3r8lg,1321
@@ -508,7 +508,7 @@ reconcile/utils/sqs_gateway.py,sha256=gFl9DM4DmGnptuxTOe4lS3YTyE80eSAvK42ljS8h4d
508
508
  reconcile/utils/state.py,sha256=_SmE7fOEReET3iy9jRQ1pyuaJebg5962Zs9Iy1dzTJk,9530
509
509
  reconcile/utils/structs.py,sha256=LcbLEg8WxfRqM6nW7NhcWN0YeqF7SQzxOgntmLs1SgY,352
510
510
  reconcile/utils/template.py,sha256=wTvRU4AnAV_o042tD4Mwls2dwWMuk7MKnde3MaCjaYg,331
511
- reconcile/utils/terraform_client.py,sha256=DUUuqKiTOWGQ3tj8zCyJP_7v45VWi1o5iGvqe3Dunco,30544
511
+ reconcile/utils/terraform_client.py,sha256=Sy125P0LybUUkI9_BTK00m_Q4-Oc6GOAPBAQ6aRirCQ,33329
512
512
  reconcile/utils/terrascript_aws_client.py,sha256=FP4LSlKynxAjdEqfotsML7fg6lFyKktXcWQrLghFuU0,260201
513
513
  reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM,344
514
514
  reconcile/utils/unleash.py,sha256=QGANGA8BHG7oC_bt39c2M7uRa2ycjzmahN8_m7Zovos,3094
@@ -577,8 +577,8 @@ tools/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
577
577
  tools/test/test_qontract_cli.py,sha256=awwTHEc2DWlykuqGIYM0WOBoSL0KRnOraCLk3C7izis,1401
578
578
  tools/test/test_sd_app_sre_alert_report.py,sha256=JeLhgzpKCPgLvptwg_4ZvJHLVWKNG1T5845HXTkMBxA,1826
579
579
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
580
- qontract_reconcile-0.10.1rc216.dist-info/METADATA,sha256=ftHv5boIaF1JALKk5Ee5kSTEQVvbQSluaTFltsAmlsQ,2328
581
- qontract_reconcile-0.10.1rc216.dist-info/WHEEL,sha256=AtBG6SXL3KF_v0NxLf0ehyVOh0cold-JbJYXNGorC6Q,92
582
- qontract_reconcile-0.10.1rc216.dist-info/entry_points.txt,sha256=ErVY2Jp-0Rtuq5KOtMlW5yvna4nIEuc_1YbEdEdcy9o,301
583
- qontract_reconcile-0.10.1rc216.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
584
- qontract_reconcile-0.10.1rc216.dist-info/RECORD,,
580
+ qontract_reconcile-0.10.1rc218.dist-info/METADATA,sha256=NYrL4Jw0x9csqevY8_axCXnpuJXmobKSqauptV79qqY,2328
581
+ qontract_reconcile-0.10.1rc218.dist-info/WHEEL,sha256=AtBG6SXL3KF_v0NxLf0ehyVOh0cold-JbJYXNGorC6Q,92
582
+ qontract_reconcile-0.10.1rc218.dist-info/entry_points.txt,sha256=ErVY2Jp-0Rtuq5KOtMlW5yvna4nIEuc_1YbEdEdcy9o,301
583
+ qontract_reconcile-0.10.1rc218.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
584
+ qontract_reconcile-0.10.1rc218.dist-info/RECORD,,
@@ -372,9 +372,8 @@ def run(
372
372
  # e.g. gitlab-housekeeper rejects direct lgtm labels and the review-queue
373
373
  # skips MRs with this label
374
374
  if SELF_SERVICEABLE in labels:
375
- gl.remove_label_from_merge_request(
376
- gitlab_merge_request_id, SELF_SERVICEABLE
377
- )
375
+ merge_request = gl.get_merge_request(gitlab_merge_request_id)
376
+ gl.remove_label(merge_request, SELF_SERVICEABLE)
378
377
 
379
378
  except BaseException:
380
379
  logging.error(traceback.format_exc())
@@ -80,7 +80,7 @@ class GitlabForkCompliance:
80
80
  # it is set
81
81
  mr_labels = self.gl_cli.get_merge_request_labels(self.mr.iid)
82
82
  if BLOCKED_BOT_ACCESS in mr_labels:
83
- self.gl_cli.remove_label_from_merge_request(self.mr.iid, BLOCKED_BOT_ACCESS)
83
+ self.gl_cli.remove_label(self.mr, BLOCKED_BOT_ACCESS)
84
84
 
85
85
  sys.exit(self.exit_code)
86
86
 
@@ -214,8 +214,7 @@ def handle_stale_items(
214
214
  now = datetime.utcnow()
215
215
  for item in items:
216
216
  item_iid = item.attributes.get("iid")
217
- item_labels = get_labels(item, gl)
218
- if AUTO_MERGE in item_labels:
217
+ if AUTO_MERGE in item.labels:
219
218
  if item.merge_status == MRStatus.UNCHECKED:
220
219
  # this call triggers a status recheck
221
220
  item = gl.get_merge_request(item_iid)
@@ -227,16 +226,16 @@ def handle_stale_items(
227
226
  current_interval = now.date() - update_date.date()
228
227
  if current_interval > timedelta(days=days_interval):
229
228
  # if item does not have 'stale' label - add it
230
- if LABEL not in item_labels:
229
+ if LABEL not in item.labels:
231
230
  logging.info(["add_label", gl.project.name, item_type, item_iid, LABEL])
232
231
  if not dry_run:
233
- gl.add_label(item, item_type, LABEL)
232
+ gl.add_label(item, LABEL)
234
233
  # if item has 'stale' label - close it
235
234
  else:
236
235
  close_item(dry_run, gl, enable_closing, item_type, item)
237
236
  # if item is under days_interval
238
237
  else:
239
- if LABEL not in item_labels:
238
+ if LABEL not in item.labels:
240
239
  continue
241
240
 
242
241
  # if item has 'stale' label - check the notes
@@ -261,7 +260,7 @@ def handle_stale_items(
261
260
  ["remove_label", gl.project.name, item_type, item_iid, LABEL]
262
261
  )
263
262
  if not dry_run:
264
- gl.remove_label(item, item_type, LABEL)
263
+ gl.remove_label(item, LABEL)
265
264
 
266
265
 
267
266
  def is_good_to_merge(labels):
@@ -277,21 +276,6 @@ def is_rebased(mr, gl: GitLabApi) -> bool:
277
276
  return len(result["commits"]) == 0
278
277
 
279
278
 
280
- def get_labels(mr: ProjectMergeRequest, gl: GitLabApi) -> list[str]:
281
- """
282
- This function used to contain logic for checking if labels were empty and calling
283
- gl.get_merge_request_labels() if they were missing because there were reports of the
284
- label attribute being empty sometimes when it shouldn't be. This was an expensive
285
- approach, increasing the runtime of the integration by something around 20-30%.
286
- Through investigation in APPSRE-6653 it was determined this no longer appears to be
287
- an issue.
288
-
289
- This is being left to continue to abstract the way that labels are pulled in case
290
- this becomes an issue again in the future.
291
- """
292
- return mr.attributes.get("labels")
293
-
294
-
295
279
  def get_merge_requests(
296
280
  dry_run: bool,
297
281
  gl: GitLabApi,
@@ -324,7 +308,7 @@ def preprocess_merge_requests(
324
308
  if len(mr.commits()) == 0:
325
309
  continue
326
310
 
327
- labels = get_labels(mr, gl)
311
+ labels = mr.labels
328
312
  if not labels:
329
313
  continue
330
314
 
@@ -336,7 +320,7 @@ def preprocess_merge_requests(
336
320
  + "suitable for self serviceable MRs. removing 'lgtm' label"
337
321
  )
338
322
  if not dry_run:
339
- gl.remove_label_from_merge_request(mr.iid, LGTM)
323
+ gl.remove_label(mr, LGTM)
340
324
  continue
341
325
 
342
326
  label_events = gl.get_merge_request_label_events(mr)
@@ -366,18 +350,17 @@ def preprocess_merge_requests(
366
350
  approved_by = added_by
367
351
 
368
352
  for bad_label in labels_by_unauthorized_users - labels_by_authorized_users:
369
- if bad_label not in labels:
353
+ if bad_label not in mr.labels:
370
354
  continue
371
355
  logging.warning(
372
356
  f"[{gl.project.name}/{mr.iid}] someone added a label who "
373
357
  f"isn't allowed. removing label {bad_label}"
374
358
  )
375
- # Remove bad_label from the cached labels list. Otherwise, we may face a caching bug
376
- labels.remove(bad_label)
377
359
  if not dry_run:
378
- gl.remove_label_from_merge_request(mr.iid, bad_label)
360
+ # TODO: optimize this to remove all bad labels at once
361
+ gl.remove_label(mr, bad_label)
379
362
 
380
- if not is_good_to_merge(labels):
363
+ if not is_good_to_merge(mr.labels):
381
364
  continue
382
365
 
383
366
  label_priority = min(
@@ -540,7 +523,7 @@ def merge_merge_requests(
540
523
  if not dry_run and merges < merge_limit:
541
524
  try:
542
525
  mr.merge()
543
- labels = get_labels(mr, gl)
526
+ labels = mr.labels
544
527
  merged_merge_requests.labels(
545
528
  project_id=mr.target_project_id,
546
529
  self_service=SELF_SERVICEABLE in labels,
@@ -370,7 +370,7 @@ def act(repo, dry_run, instance, settings, defer=None):
370
370
  f"- removing approval"
371
371
  ]
372
372
  )
373
- gitlab_cli.remove_label_from_merge_request(mr.iid, APPROVED)
373
+ gitlab_cli.remove_label(mr, APPROVED)
374
374
 
375
375
  if approval_status["report"] is not None:
376
376
  _LOG.info(
@@ -382,7 +382,7 @@ def act(repo, dry_run, instance, settings, defer=None):
382
382
  )
383
383
 
384
384
  if not dry_run:
385
- gitlab_cli.remove_label_from_merge_request(mr.iid, APPROVED)
385
+ gitlab_cli.remove_label(mr, APPROVED)
386
386
  mr.notes.create({"body": approval_status["report"]})
387
387
  continue
388
388
 
@@ -177,9 +177,7 @@ def can_be_merged_merge_request() -> ProjectMergeRequest:
177
177
  mr.merge_status = "can_be_merged"
178
178
  mr.work_in_progress = False
179
179
  mr.commits.return_value = [create_autospec(ProjectCommit)]
180
- mr.attributes = {
181
- "labels": ["lgtm"],
182
- }
180
+ mr.labels = ["lgtm"]
183
181
  mr.iid = 1
184
182
  mr.target_project_id = 3
185
183
  mr.author = {"username": "user"}
@@ -14,6 +14,7 @@ import gitlab
14
14
  import urllib3
15
15
  from gitlab.v4.objects import (
16
16
  CurrentUser,
17
+ ProjectIssue,
17
18
  ProjectMergeRequest,
18
19
  ProjectMergeRequestNote,
19
20
  )
@@ -449,14 +450,6 @@ class GitLabApi: # pylint: disable=too-many-public-methods
449
450
  merge_request = self.project.mergerequests.get(mr_id)
450
451
  self.update_labels(merge_request, "merge-request", labels)
451
452
 
452
- def remove_label_from_merge_request(self, mr_id, label):
453
- gitlab_request.labels(integration=INTEGRATION_NAME).inc()
454
- merge_request = self.project.mergerequests.get(mr_id)
455
- labels = merge_request.attributes.get("labels")
456
- if label in labels:
457
- labels.remove(label)
458
- self.update_labels(merge_request, "merge-request", labels)
459
-
460
453
  def add_comment_to_merge_request(self, mr_id, body):
461
454
  gitlab_request.labels(integration=INTEGRATION_NAME).inc()
462
455
  merge_request = self.project.mergerequests.get(mr_id)
@@ -480,20 +473,34 @@ class GitLabApi: # pylint: disable=too-many-public-methods
480
473
  gitlab_request.labels(integration=INTEGRATION_NAME).inc()
481
474
  self.project.labels.create({"name": label_text, "color": label_color})
482
475
 
483
- def add_label(self, item, item_type, label):
484
- note_body = (
485
- "item has been marked as {0}. " "to remove say `/{0} cancel`"
486
- ).format(label)
487
- labels = item.attributes.get("labels")
476
+ @staticmethod
477
+ def add_label(
478
+ item: ProjectMergeRequest | ProjectIssue,
479
+ label: str,
480
+ ):
481
+ labels = item.labels
482
+ if label in labels:
483
+ return
488
484
  labels.append(label)
485
+ note_body = (
486
+ f"item has been marked as {label}. " f"to remove say `/{label} cancel`"
487
+ )
489
488
  gitlab_request.labels(integration=INTEGRATION_NAME).inc()
490
489
  item.notes.create({"body": note_body})
491
- self.update_labels(item, item_type, labels)
490
+ gitlab_request.labels(integration=INTEGRATION_NAME).inc()
491
+ item.save()
492
492
 
493
- def remove_label(self, item, item_type, label):
494
- labels = item.attributes.get("labels")
493
+ @staticmethod
494
+ def remove_label(
495
+ item: ProjectMergeRequest | ProjectIssue,
496
+ label: str,
497
+ ):
498
+ labels = item.labels
499
+ if label not in labels:
500
+ return
495
501
  labels.remove(label)
496
- self.update_labels(item, item_type, labels)
502
+ gitlab_request.labels(integration=INTEGRATION_NAME).inc()
503
+ item.save()
497
504
 
498
505
  def update_labels(self, item, item_type, labels):
499
506
  if item_type == "issue":
@@ -1,11 +1,17 @@
1
1
  import json
2
2
  import logging
3
+ import os
4
+ import re
3
5
  import shutil
6
+ import tempfile
7
+ import typing
4
8
  from collections import defaultdict
5
9
  from collections.abc import (
10
+ Generator,
6
11
  Iterable,
7
12
  Mapping,
8
13
  )
14
+ from contextlib import contextmanager
9
15
  from dataclasses import dataclass
10
16
  from datetime import (
11
17
  datetime,
@@ -18,6 +24,7 @@ from typing import (
18
24
  cast,
19
25
  )
20
26
 
27
+ import python_terraform
21
28
  from botocore.errorfactory import ClientError
22
29
  from python_terraform import (
23
30
  IsFlagged,
@@ -39,6 +46,8 @@ from reconcile.utils.external_resource_spec import (
39
46
 
40
47
  ALLOWED_TF_SHOW_FORMAT_VERSION = "0.1"
41
48
  DATE_FORMAT = "%Y-%m-%d"
49
+ PROVIDER_LOG_REGEX = r""".*(?:\[INFO]|\[WARN]|\[ERROR]).+(?:\[WARN]|\[ERROR]).*"""
50
+ TERRAFORM_LOG_LEVEL = "TRACE" # can change to INFO after tf 0.15
42
51
 
43
52
 
44
53
  @dataclass
@@ -72,6 +81,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
72
81
  self.thread_pool_size = thread_pool_size
73
82
  self._aws_api = aws_api
74
83
  self._log_lock = Lock()
84
+ self._tf_env_lock = Lock()
75
85
  self.should_apply = False
76
86
 
77
87
  self.init_specs()
@@ -110,30 +120,76 @@ class TerraformClient: # pylint: disable=too-many-public-methods
110
120
 
111
121
  def init_specs(self):
112
122
  wd_specs = [{"name": name, "wd": wd} for name, wd in self.working_dirs.items()]
113
- results = threaded.run(self.terraform_init, wd_specs, self.thread_pool_size)
123
+ with self._monkey_patch_terraform_env():
124
+ results = threaded.run(self.terraform_init, wd_specs, self.thread_pool_size)
114
125
  self.specs = [{"name": name, "tf": tf} for name, tf in results]
115
126
 
127
+ @contextmanager
128
+ def _monkey_patch_terraform_env(self) -> Generator[None, None, None]:
129
+ # https://github.com/beelit94/python-terraform/blob/release/0.10.1/python_terraform/__init__.py#L290
130
+ old_copy = python_terraform.os.environ.copy
131
+
132
+ def new_copy():
133
+ result = old_copy()
134
+ self._tf_env_lock.release()
135
+ return result
136
+
137
+ python_terraform.os.environ.copy = new_copy
138
+
139
+ try:
140
+ yield
141
+ finally:
142
+ python_terraform.os.environ.copy = old_copy
143
+
144
+ @contextmanager
145
+ def _terraform_log_file(self, working_dir: str) -> Generator[typing.IO, None, None]:
146
+ with tempfile.NamedTemporaryFile(dir=working_dir) as f:
147
+ # lock is released in terraform method monkey patched in _monkey_patch_terraform_env
148
+ self._tf_env_lock.acquire()
149
+ os.environ["TF_LOG"] = TERRAFORM_LOG_LEVEL
150
+ os.environ["TF_LOG_PATH"] = f.name
151
+ try:
152
+ yield f
153
+ except Exception:
154
+ # release lock if exception raised before monkey patched os.environ.copy called
155
+ if self._tf_env_lock.locked():
156
+ self._tf_env_lock.release()
157
+ raise
158
+ finally:
159
+ with self._tf_env_lock:
160
+ if "TF_LOG" in os.environ:
161
+ del os.environ["TF_LOG"]
162
+ if "TF_LOG_PATH" in os.environ:
163
+ del os.environ["TF_LOG_PATH"]
164
+
116
165
  @retry(exceptions=TerraformCommandError)
117
166
  def terraform_init(self, init_spec):
118
167
  name = init_spec["name"]
119
168
  wd = init_spec["wd"]
120
- tf = Terraform(working_dir=wd)
121
- return_code, stdout, stderr = tf.init()
122
- error = self.check_output(name, "init", return_code, stdout, stderr)
169
+ tf = Terraform(working_dir=wd, is_env_vars_included=True)
170
+ with self._terraform_log_file(tf.working_dir) as f:
171
+ return_code, stdout, stderr = tf.init()
172
+ log = f.read().decode("utf-8")
173
+ error = self.check_output(name, "init", return_code, stdout, stderr, log)
123
174
  if error:
124
175
  raise TerraformCommandError(return_code, "init", out=stdout, err=stderr)
125
176
  return name, tf
126
177
 
127
178
  def init_outputs(self):
128
- results = threaded.run(self.terraform_output, self.specs, self.thread_pool_size)
179
+ with self._monkey_patch_terraform_env():
180
+ results = threaded.run(
181
+ self.terraform_output, self.specs, self.thread_pool_size
182
+ )
129
183
  self.outputs = dict(results)
130
184
 
131
185
  @retry(exceptions=TerraformCommandError)
132
186
  def terraform_output(self, spec):
133
187
  name = spec["name"]
134
188
  tf = spec["tf"]
135
- return_code, stdout, stderr = tf.output_cmd(json=IsFlagged)
136
- error = self.check_output(name, "output", return_code, stdout, stderr)
189
+ with self._terraform_log_file(tf.working_dir) as f:
190
+ return_code, stdout, stderr = tf.output_cmd(json=IsFlagged)
191
+ log = f.read().decode("utf-8")
192
+ error = self.check_output(name, "output", return_code, stdout, stderr, log)
137
193
  no_output_error = (
138
194
  "The module root could not be found. There is nothing to output."
139
195
  )
@@ -150,12 +206,13 @@ class TerraformClient: # pylint: disable=too-many-public-methods
150
206
  def plan(self, enable_deletion):
151
207
  errors = False
152
208
  disabled_deletions_detected = False
153
- results = threaded.run(
154
- self.terraform_plan,
155
- self.specs,
156
- self.thread_pool_size,
157
- enable_deletion=enable_deletion,
158
- )
209
+ with self._monkey_patch_terraform_env():
210
+ results = threaded.run(
211
+ self.terraform_plan,
212
+ self.specs,
213
+ self.thread_pool_size,
214
+ enable_deletion=enable_deletion,
215
+ )
159
216
 
160
217
  self.created_users = []
161
218
  for disabled_deletion_detected, created_users, error in results:
@@ -172,10 +229,12 @@ class TerraformClient: # pylint: disable=too-many-public-methods
172
229
  ) -> tuple[bool, list[AccountUser], bool]:
173
230
  name = plan_spec["name"]
174
231
  tf = plan_spec["tf"]
175
- return_code, stdout, stderr = tf.plan(
176
- detailed_exitcode=False, parallelism=self.parallelism, out=name
177
- )
178
- error = self.check_output(name, "plan", return_code, stdout, stderr)
232
+ with self._terraform_log_file(tf.working_dir) as f:
233
+ return_code, stdout, stderr = tf.plan(
234
+ detailed_exitcode=False, parallelism=self.parallelism, out=name
235
+ )
236
+ log = f.read().decode("utf-8")
237
+ error = self.check_output(name, "plan", return_code, stdout, stderr, log)
179
238
  disabled_deletion_detected, created_users = self.log_plan_diff(
180
239
  name, tf, enable_deletion
181
240
  )
@@ -370,7 +429,10 @@ class TerraformClient: # pylint: disable=too-many-public-methods
370
429
  def apply(self):
371
430
  errors = False
372
431
 
373
- results = threaded.run(self.terraform_apply, self.specs, self.thread_pool_size)
432
+ with self._monkey_patch_terraform_env():
433
+ results = threaded.run(
434
+ self.terraform_apply, self.specs, self.thread_pool_size
435
+ )
374
436
 
375
437
  for error in results:
376
438
  if error:
@@ -380,10 +442,12 @@ class TerraformClient: # pylint: disable=too-many-public-methods
380
442
  def terraform_apply(self, apply_spec):
381
443
  name = apply_spec["name"]
382
444
  tf = apply_spec["tf"]
383
- # adding var=None to allow applying the saved plan
384
- # https://github.com/beelit94/python-terraform/issues/67
385
- return_code, stdout, stderr = tf.apply(dir_or_plan=name, var=None)
386
- error = self.check_output(name, "apply", return_code, stdout, stderr)
445
+ with self._terraform_log_file(tf.working_dir) as f:
446
+ # adding var=None to allow applying the saved plan
447
+ # https://github.com/beelit94/python-terraform/issues/67
448
+ return_code, stdout, stderr = tf.apply(dir_or_plan=name, var=None)
449
+ log = f.read().decode("utf-8")
450
+ error = self.check_output(name, "apply", return_code, stdout, stderr, log)
387
451
  return error
388
452
 
389
453
  def get_terraform_output_secrets(self) -> dict[str, dict[str, dict[str, str]]]:
@@ -553,22 +617,26 @@ class TerraformClient: # pylint: disable=too-many-public-methods
553
617
  name: str,
554
618
  cmd: str,
555
619
  return_code: int,
556
- stdout: list[str],
557
- stderr: list[str],
620
+ stdout: str,
621
+ stderr: str,
622
+ log: str,
558
623
  ) -> bool:
559
- error_occured = False
624
+ error_occured = return_code != 0
560
625
  line_format = "[{} - {}] {}"
561
- stdout, stderr = self.split_to_lines(stdout, stderr)
626
+ stdout, stderr, log = self.split_to_lines(stdout, stderr, log)
627
+ provider_log_re = re.compile(PROVIDER_LOG_REGEX)
562
628
  with self._log_lock:
563
629
  for line in stdout:
564
630
  logging.debug(line_format.format(name, cmd, line))
565
- if return_code == 0:
631
+ if error_occured:
566
632
  for line in stderr:
567
- logging.warning(line_format.format(name, cmd, line))
633
+ logging.error(line_format.format(name, cmd, line))
568
634
  else:
569
635
  for line in stderr:
570
- logging.error(line_format.format(name, cmd, line))
571
- error_occured = True
636
+ logging.warning(line_format.format(name, cmd, line))
637
+ for line in log:
638
+ if provider_log_re.match(line):
639
+ logging.warning(line_format.format(name, cmd, line))
572
640
  return error_occured
573
641
 
574
642
  @staticmethod