qontract-reconcile 0.10.1rc781__py3-none-any.whl → 0.10.1rc783__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.1rc781
3
+ Version: 0.10.1rc783
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
@@ -10,7 +10,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
10
10
  reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
11
11
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
12
12
  reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
13
- reconcile/cli.py,sha256=5x9LADlVvoEB6Wen6NZeLZnGIj439voLz5L6ccxRxds,99574
13
+ reconcile/cli.py,sha256=uMjslj9XkN1UFJ3k-CuHlTz4MeyXMk6gK1BkWfg6Kt4,99967
14
14
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
15
15
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
16
16
  reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
@@ -114,7 +114,7 @@ reconcile/terraform_cloudflare_resources.py,sha256=EbQQaoDnZ7brvRCpbFtwlD7KLk2hD
114
114
  reconcile/terraform_cloudflare_users.py,sha256=1EbTHwJgiPkJpMP-Ag340QNgGK3mXn3dcC3DpLakudM,13987
115
115
  reconcile/terraform_repo.py,sha256=xkp5EiRQ7cz-IquXiBq5plvBQLf910tqywKK0B_QlPM,16271
116
116
  reconcile/terraform_resources.py,sha256=BN8XuJwjOt1ztruEAHydkd0YiBlb3fHZ7n0snZtRhck,19356
117
- reconcile/terraform_tgw_attachments.py,sha256=k9Lf0ST65gmI6aUV6HnvxSGcKL7MGx_lN22OXuRGH9Y,16224
117
+ reconcile/terraform_tgw_attachments.py,sha256=S5IP7RmnVuVSOIPXFlUX2srGBrBBeH2uM4J6ewHCvOQ,18797
118
118
  reconcile/terraform_users.py,sha256=9rgbM572LfmOSnV3uCP20G_Cw6T7due94g8rhhiz904,10225
119
119
  reconcile/terraform_vpc_peerings.py,sha256=rnDH1u93OyzrBM8Hib0HwSnlxZtx4ScRQaZAcn3mx-k,25402
120
120
  reconcile/vault_replication.py,sha256=79GZ_kCimPoQcxkdhkWTQxPOAa46E0mNhf05s_Mk5so,17385
@@ -523,7 +523,7 @@ reconcile/test/test_terraform_cloudflare_resources.py,sha256=NK_uktyWihkQ3gMN4bC
523
523
  reconcile/test/test_terraform_cloudflare_users.py,sha256=RAFtMMdqZha3jNnNNsqbNQQUDSqUzdoM63rCw7fs4Fo,27456
524
524
  reconcile/test/test_terraform_repo.py,sha256=j9mLfwiK707U2KRxYpvzAbOYywk__pL9SXAH3xbP1t8,12184
525
525
  reconcile/test/test_terraform_resources.py,sha256=EFCqPI5_G8hPRh1zmnU91o8wMeT2qK1CabDUa_X1rSk,15283
526
- reconcile/test/test_terraform_tgw_attachments.py,sha256=cAq6exc-K-jtLla1CZUZQzVnBkyDnIlL7jybnddhLKc,36861
526
+ reconcile/test/test_terraform_tgw_attachments.py,sha256=rHZHUtDxewpKsRj3nfm2bZ2JoQ4CWiN2nQM-SWkMopg,41047
527
527
  reconcile/test/test_terraform_users.py,sha256=XOAfGvITCJPI1LTlISmHbA4ONMQMkxYUMTsny7pQCFw,4319
528
528
  reconcile/test/test_terraform_vpc_peerings.py,sha256=ubcsKh0TrUIwuI1-W3ETIgzsFvzAyeoFmEJFC-IK6JY,20538
529
529
  reconcile/test/test_terraform_vpc_peerings_build_desired_state.py,sha256=DAfpb12I0PlqnuVUHK2vh4LH4d1OylT3H2GE_3TGZZI,47852
@@ -785,8 +785,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
785
785
  tools/test/test_qontract_cli.py,sha256=w2l4BHB09k1d-BGJ1jBUNCqDv7zkqYrMHojQXg-21kQ,4155
786
786
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
787
787
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
788
- qontract_reconcile-0.10.1rc781.dist-info/METADATA,sha256=U8z82KU9PoP9inuu8tIIXkd2XaKyf1VWzyrbI2seSYQ,2382
789
- qontract_reconcile-0.10.1rc781.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
790
- qontract_reconcile-0.10.1rc781.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
791
- qontract_reconcile-0.10.1rc781.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
792
- qontract_reconcile-0.10.1rc781.dist-info/RECORD,,
788
+ qontract_reconcile-0.10.1rc783.dist-info/METADATA,sha256=SwZsjjF0UyVKhYaUdO-pvRDKF2nPhywOgRc0OcA-XLA,2382
789
+ qontract_reconcile-0.10.1rc783.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
790
+ qontract_reconcile-0.10.1rc783.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
791
+ qontract_reconcile-0.10.1rc783.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
792
+ qontract_reconcile-0.10.1rc783.dist-info/RECORD,,
reconcile/cli.py CHANGED
@@ -2318,6 +2318,9 @@ def vpc_peerings_validator(ctx):
2318
2318
  @threaded()
2319
2319
  @binary(["terraform", "git"])
2320
2320
  @binary_version("terraform", ["version"], TERRAFORM_VERSION_REGEX, TERRAFORM_VERSION)
2321
+ @enable_extended_early_exit
2322
+ @extended_early_exit_cache_ttl_seconds
2323
+ @log_cached_log_output
2321
2324
  @enable_deletion(default=False)
2322
2325
  @account_name
2323
2326
  @click.pass_context
@@ -2327,6 +2330,9 @@ def terraform_tgw_attachments(
2327
2330
  enable_deletion,
2328
2331
  thread_pool_size,
2329
2332
  account_name,
2333
+ enable_extended_early_exit,
2334
+ extended_early_exit_cache_ttl_seconds,
2335
+ log_cached_log_output,
2330
2336
  ):
2331
2337
  import reconcile.terraform_tgw_attachments
2332
2338
 
@@ -2339,6 +2345,9 @@ def terraform_tgw_attachments(
2339
2345
  enable_deletion,
2340
2346
  thread_pool_size,
2341
2347
  account_name=account_name,
2348
+ enable_extended_early_exit=enable_extended_early_exit,
2349
+ extended_early_exit_cache_ttl_seconds=extended_early_exit_cache_ttl_seconds,
2350
+ log_cached_log_output=log_cached_log_output,
2342
2351
  )
2343
2352
 
2344
2353
 
@@ -8,6 +8,7 @@ from collections.abc import (
8
8
  from typing import (
9
9
  Any,
10
10
  Optional,
11
+ TypedDict,
11
12
  Union,
12
13
  cast,
13
14
  )
@@ -40,15 +41,20 @@ from reconcile.utils import gql
40
41
  from reconcile.utils.aws_api import AWSApi
41
42
  from reconcile.utils.defer import defer
42
43
  from reconcile.utils.disabled_integrations import integration_is_enabled
44
+ from reconcile.utils.extended_early_exit import (
45
+ ExtendedEarlyExitRunnerResult,
46
+ extended_early_exit_run,
47
+ )
43
48
  from reconcile.utils.ocm import (
44
49
  OCM,
45
50
  OCMMap,
46
51
  )
47
52
  from reconcile.utils.runtime.integration import DesiredStateShardConfig
48
- from reconcile.utils.secret_reader import create_secret_reader
53
+ from reconcile.utils.secret_reader import SecretReaderBase, create_secret_reader
49
54
  from reconcile.utils.semver_helper import make_semver
50
55
  from reconcile.utils.terraform_client import TerraformClient as Terraform
51
56
  from reconcile.utils.terrascript_aws_client import TerrascriptClient as Terrascript
57
+ from reconcile.utils.unleash import get_feature_toggle_state
52
58
 
53
59
  QONTRACT_INTEGRATION = "terraform_tgw_attachments"
54
60
  QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
@@ -103,6 +109,17 @@ class DesiredStateDataSource(BaseModel):
103
109
  accounts: list[AWSAccountV1]
104
110
 
105
111
 
112
+ class CacheSource(TypedDict):
113
+ terraform_configurations: dict[str, str]
114
+
115
+
116
+ class RunnerParams(TypedDict):
117
+ terraform_client: Terraform
118
+ terrascript_client: Terrascript
119
+ dry_run: bool
120
+ enable_deletion: bool
121
+
122
+
106
123
  def _build_desired_state_tgw_attachments(
107
124
  clusters: Iterable[ClusterV1],
108
125
  ocm_map: Optional[OCMMap],
@@ -402,37 +419,19 @@ def _fetch_desired_state_data_source(
402
419
  )
403
420
 
404
421
 
405
- @defer
406
- def run(
407
- dry_run: bool,
408
- print_to_file: Optional[str] = None,
409
- enable_deletion: bool = False,
422
+ def setup(
423
+ account_name: Optional[str],
424
+ desired_state_data_source: DesiredStateDataSource,
425
+ tgw_accounts: list[dict[str, Any]],
410
426
  thread_pool_size: int = 10,
411
- account_name: Optional[str] = None,
412
- defer: Optional[Callable] = None,
413
- ) -> None:
414
- desired_state_data_source = _fetch_desired_state_data_source(account_name)
427
+ print_to_file: Optional[str] = None,
428
+ ) -> tuple[SecretReaderBase, AWSApi, Terraform, Terrascript]:
415
429
  tgw_clusters = desired_state_data_source.clusters
416
430
  all_accounts = [a.dict(by_alias=True) for a in desired_state_data_source.accounts]
417
431
  account_by_name = {a["name"]: a for a in all_accounts}
418
- tgw_accounts = [
419
- a.dict(by_alias=True)
420
- for a in _filter_tgw_accounts(desired_state_data_source.accounts, tgw_clusters)
421
- if not account_name or account_name == a.name
422
- ]
423
-
424
- if not tgw_accounts:
425
- logging.warning(
426
- f"No participating AWS accounts found, consider disabling this integration, account name: {account_name}"
427
- )
428
- return
429
-
430
432
  vault_settings = get_app_interface_vault_settings()
431
433
  secret_reader = create_secret_reader(vault_settings.vault)
432
434
  aws_api = AWSApi(1, all_accounts, secret_reader=secret_reader, init_users=False)
433
- if defer:
434
- defer(aws_api.cleanup)
435
-
436
435
  ocm_map = _build_ocm_map(desired_state_data_source.clusters, vault_settings)
437
436
  desired_state, err = _build_desired_state_tgw_attachments(
438
437
  desired_state_data_source.clusters,
@@ -463,10 +462,6 @@ def run(
463
462
  desired_state,
464
463
  print_to_file,
465
464
  )
466
-
467
- if print_to_file:
468
- return
469
-
470
465
  tf = Terraform(
471
466
  QONTRACT_INTEGRATION,
472
467
  QONTRACT_INTEGRATION_VERSION,
@@ -476,22 +471,95 @@ def run(
476
471
  thread_pool_size,
477
472
  aws_api,
478
473
  )
474
+ return secret_reader, aws_api, tf, ts
479
475
 
480
- if defer:
481
- defer(tf.cleanup)
482
476
 
483
- disabled_deletions_detected, err = tf.plan(enable_deletion)
477
+ def runner(
478
+ dry_run: bool,
479
+ terraform_client: Terraform,
480
+ terrascript_client: Terrascript,
481
+ enable_deletion: bool = False,
482
+ ) -> ExtendedEarlyExitRunnerResult:
483
+ disabled_deletions_detected, err = terraform_client.plan(enable_deletion)
484
484
  if err:
485
485
  raise RuntimeError("Error running terraform plan")
486
486
  if disabled_deletions_detected:
487
487
  raise RuntimeError("Disabled deletions detected running terraform plan")
488
+ if not dry_run:
489
+ err = terraform_client.apply()
490
+ if err:
491
+ raise RuntimeError("Error running terraform apply")
492
+ return ExtendedEarlyExitRunnerResult(
493
+ payload=terrascript_client.terraform_configurations(),
494
+ applied_count=terraform_client.apply_count,
495
+ )
488
496
 
489
- if dry_run:
490
- return
491
497
 
492
- err = tf.apply()
493
- if err:
494
- raise RuntimeError("Error running terraform apply")
498
+ @defer
499
+ def run(
500
+ dry_run: bool,
501
+ print_to_file: Optional[str] = None,
502
+ enable_deletion: bool = False,
503
+ thread_pool_size: int = 10,
504
+ account_name: Optional[str] = None,
505
+ defer: Optional[Callable] = None,
506
+ enable_extended_early_exit: bool = False,
507
+ extended_early_exit_cache_ttl_seconds: int = 3600,
508
+ log_cached_log_output: bool = False,
509
+ ) -> None:
510
+ desired_state_data_source = _fetch_desired_state_data_source(account_name)
511
+ tgw_accounts = [
512
+ a.dict(by_alias=True)
513
+ for a in _filter_tgw_accounts(
514
+ desired_state_data_source.accounts, desired_state_data_source.clusters
515
+ )
516
+ if not account_name or account_name == a.name
517
+ ]
518
+ if not tgw_accounts:
519
+ logging.warning(
520
+ f"No participating AWS accounts found, consider disabling this integration, account name: {account_name}"
521
+ )
522
+ return
523
+ secret_reader, aws_api, tf, ts = setup(
524
+ desired_state_data_source=desired_state_data_source,
525
+ account_name=account_name,
526
+ tgw_accounts=tgw_accounts,
527
+ thread_pool_size=thread_pool_size,
528
+ print_to_file=print_to_file,
529
+ )
530
+ if defer:
531
+ defer(aws_api.cleanup)
532
+ defer(tf.cleanup)
533
+ if print_to_file:
534
+ return
535
+ runner_params: RunnerParams = dict(
536
+ terraform_client=tf,
537
+ terrascript_client=ts,
538
+ enable_deletion=enable_deletion,
539
+ dry_run=dry_run,
540
+ )
541
+ if enable_extended_early_exit and get_feature_toggle_state(
542
+ "terraform-tgw-attachments-extended-early-exit",
543
+ default=False,
544
+ ):
545
+ cache_source = CacheSource(
546
+ terraform_configurations=ts.terraform_configurations(),
547
+ )
548
+ extended_early_exit_run(
549
+ integration=QONTRACT_INTEGRATION,
550
+ integration_version=QONTRACT_INTEGRATION_VERSION,
551
+ dry_run=dry_run,
552
+ cache_source=cache_source,
553
+ shard="_".join(account_name) if account_name else "",
554
+ ttl_seconds=extended_early_exit_cache_ttl_seconds,
555
+ logger=logging.getLogger(),
556
+ runner=runner,
557
+ runner_params=runner_params,
558
+ secret_reader=secret_reader,
559
+ log_cached_log_output=log_cached_log_output,
560
+ )
561
+ else:
562
+ runner(**runner_params)
495
563
 
496
564
 
497
565
  def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
@@ -509,6 +509,7 @@ def _setup_mocks(
509
509
  vpc_details: Optional[Mapping] = None,
510
510
  tgws: Optional[Iterable] = None,
511
511
  assume_role: Optional[str] = None,
512
+ feature_toggle_state: bool = True,
512
513
  ) -> dict:
513
514
  mocked_gql_api = create_autospec(GqlApi)
514
515
  mocker.patch(
@@ -564,7 +565,14 @@ def _setup_mocks(
564
565
  ).return_value
565
566
  mocked_tf.plan.return_value = (False, False)
566
567
  mocked_tf.apply.return_value = False
567
-
568
+ mocked_tf.apply_count = 0
569
+ get_feature_toggle_state = mocker.patch(
570
+ "reconcile.terraform_tgw_attachments.get_feature_toggle_state",
571
+ return_value=feature_toggle_state,
572
+ )
573
+ mock_extended_early_exit_run = mocker.patch(
574
+ "reconcile.terraform_tgw_attachments.extended_early_exit_run"
575
+ )
568
576
  mocked_logging = mocker.patch("reconcile.terraform_tgw_attachments.logging")
569
577
 
570
578
  return {
@@ -578,9 +586,126 @@ def _setup_mocks(
578
586
  "aws_api": mocked_aws_api,
579
587
  "gql_api": mocked_gql_api,
580
588
  "logging": mocked_logging,
589
+ "extended_early_exit_run": mock_extended_early_exit_run,
590
+ "get_feature_toggle_state": get_feature_toggle_state,
581
591
  }
582
592
 
583
593
 
594
+ def test_with_extended_early_exit_enabled(
595
+ mocker: MockerFixture,
596
+ app_interface_vault_settings: AppInterfaceSettingsV1,
597
+ cluster_with_tgw_connection: ClusterV1,
598
+ tgw_account: AWSAccountV1,
599
+ tgw: Mapping,
600
+ vpc_details: Mapping,
601
+ assume_role: str,
602
+ ) -> None:
603
+ mocks = _setup_mocks(
604
+ mocker,
605
+ vault_settings=app_interface_vault_settings,
606
+ clusters=[cluster_with_tgw_connection],
607
+ accounts=[tgw_account],
608
+ vpc_details=vpc_details,
609
+ tgws=[tgw],
610
+ assume_role=assume_role,
611
+ )
612
+ expected_params = integ.RunnerParams(
613
+ terraform_client=mocks["tf"],
614
+ terrascript_client=mocks["ts"],
615
+ dry_run=False,
616
+ enable_deletion=False,
617
+ )
618
+
619
+ integ.run(
620
+ False,
621
+ enable_deletion=False,
622
+ enable_extended_early_exit=True,
623
+ extended_early_exit_cache_ttl_seconds=40,
624
+ log_cached_log_output=True,
625
+ )
626
+
627
+ mocks["extended_early_exit_run"].assert_called_once_with(
628
+ integration=integ.QONTRACT_INTEGRATION,
629
+ integration_version=integ.QONTRACT_INTEGRATION_VERSION,
630
+ dry_run=False,
631
+ shard="",
632
+ cache_source=integ.CacheSource(
633
+ terraform_configurations=mocks["ts"].terraform_configurations.return_value
634
+ ),
635
+ ttl_seconds=40,
636
+ logger=mocks["logging"].getLogger.return_value,
637
+ runner=integ.runner,
638
+ runner_params=expected_params,
639
+ secret_reader=mocks["secret_reader"],
640
+ log_cached_log_output=True,
641
+ )
642
+
643
+
644
+ def test_with_extended_early_exit_disabled(
645
+ mocker: MockerFixture,
646
+ app_interface_vault_settings: AppInterfaceSettingsV1,
647
+ cluster_with_tgw_connection: ClusterV1,
648
+ tgw_account: AWSAccountV1,
649
+ tgw: Mapping,
650
+ vpc_details: Mapping,
651
+ assume_role: str,
652
+ ) -> None:
653
+ mocks = _setup_mocks(
654
+ mocker,
655
+ vault_settings=app_interface_vault_settings,
656
+ clusters=[cluster_with_tgw_connection],
657
+ accounts=[tgw_account],
658
+ vpc_details=vpc_details,
659
+ tgws=[tgw],
660
+ assume_role=assume_role,
661
+ )
662
+ integ.run(
663
+ False,
664
+ enable_deletion=False,
665
+ enable_extended_early_exit=False,
666
+ )
667
+ mocks["extended_early_exit_run"].assert_not_called()
668
+ mocks["get_app_interface_vault_settings"].assert_called_once_with()
669
+ mocks["get_clusters_with_peering"].assert_called_once_with(mocks["gql_api"])
670
+ mocks["get_aws_accounts"].assert_called_once_with(mocks["gql_api"])
671
+ mocks["tf"].plan.assert_called_once_with(False)
672
+ mocks["tf"].apply.assert_called_once()
673
+
674
+
675
+ def test_with_feature_flag_disabled(
676
+ mocker: MockerFixture,
677
+ app_interface_vault_settings: AppInterfaceSettingsV1,
678
+ cluster_with_tgw_connection: ClusterV1,
679
+ tgw_account: AWSAccountV1,
680
+ tgw: Mapping,
681
+ vpc_details: Mapping,
682
+ assume_role: str,
683
+ ) -> None:
684
+ mocks = _setup_mocks(
685
+ mocker,
686
+ vault_settings=app_interface_vault_settings,
687
+ clusters=[cluster_with_tgw_connection],
688
+ accounts=[tgw_account],
689
+ vpc_details=vpc_details,
690
+ tgws=[tgw],
691
+ assume_role=assume_role,
692
+ feature_toggle_state=False,
693
+ )
694
+ integ.run(
695
+ False,
696
+ enable_deletion=False,
697
+ enable_extended_early_exit=True,
698
+ extended_early_exit_cache_ttl_seconds=40,
699
+ log_cached_log_output=True,
700
+ )
701
+ mocks["extended_early_exit_run"].assert_not_called()
702
+ mocks["get_app_interface_vault_settings"].assert_called_once_with()
703
+ mocks["get_clusters_with_peering"].assert_called_once_with(mocks["gql_api"])
704
+ mocks["get_aws_accounts"].assert_called_once_with(mocks["gql_api"])
705
+ mocks["tf"].plan.assert_called_once_with(False)
706
+ mocks["tf"].apply.assert_called_once()
707
+
708
+
584
709
  def test_empty_run(
585
710
  mocker: MockerFixture,
586
711
  app_interface_vault_settings: AppInterfaceSettingsV1,