qontract-reconcile 0.10.1rc537__py3-none-any.whl → 0.10.1rc538__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.
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc538.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc538.dist-info}/RECORD +16 -15
- reconcile/cli.py +33 -0
- reconcile/terraform_resources.py +182 -111
- reconcile/test/test_terraform_resources.py +242 -12
- reconcile/utils/early_exit_cache.py +5 -5
- reconcile/utils/extended_early_exit.py +177 -0
- reconcile/utils/external_resources.py +2 -1
- reconcile/utils/metrics.py +7 -0
- reconcile/utils/terraform_client.py +10 -4
- reconcile/utils/terrascript_aws_client.py +11 -3
- tools/qontract_cli.py +17 -17
- tools/test/test_qontract_cli.py +1 -1
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc538.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc538.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc538.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc538.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.
|
3
|
+
Version: 0.10.1rc538
|
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
|
{qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc538.dist-info}/RECORD
RENAMED
@@ -9,7 +9,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
|
|
9
9
|
reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
|
10
10
|
reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
|
11
11
|
reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
|
12
|
-
reconcile/cli.py,sha256=
|
12
|
+
reconcile/cli.py,sha256=LFsNTM36MINQ_iDqIpXlJPrUcLUL7BUEE4GpPG2LmaQ,85546
|
13
13
|
reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
|
14
14
|
reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
|
15
15
|
reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
|
@@ -111,7 +111,7 @@ reconcile/terraform_cloudflare_dns.py,sha256=auU4bzeLwd4S8D8oqpqJbrCUoEdELXrgi7v
|
|
111
111
|
reconcile/terraform_cloudflare_resources.py,sha256=EbQQaoDnZ7brvRCpbFtwlD7KLk2hDVNcjhrJGaAywEk,15023
|
112
112
|
reconcile/terraform_cloudflare_users.py,sha256=1EbTHwJgiPkJpMP-Ag340QNgGK3mXn3dcC3DpLakudM,13987
|
113
113
|
reconcile/terraform_repo.py,sha256=c0GZFuY3rCm6VHjHqYbsgOHrEkRWKF_1LrMThsn2XDw,16127
|
114
|
-
reconcile/terraform_resources.py,sha256=
|
114
|
+
reconcile/terraform_resources.py,sha256=3-q0WyCzBbfgvfDjCEKGp09bNGgxtbzUs9jEeoLr6u4,19176
|
115
115
|
reconcile/terraform_tgw_attachments.py,sha256=_g7QSHM03YZzTU7O189S4HYtUn7WmwOBq67G4AieU24,15298
|
116
116
|
reconcile/terraform_users.py,sha256=kXRUxCUchKCP2dbXXOzctynqMii4oyCP6bYZHQTrlTg,10202
|
117
117
|
reconcile/terraform_vpc_peerings.py,sha256=rnDH1u93OyzrBM8Hib0HwSnlxZtx4ScRQaZAcn3mx-k,25402
|
@@ -440,7 +440,7 @@ reconcile/test/test_terraform_cloudflare_dns.py,sha256=aQTXX8Vr4h9aWvJZTnpZEhMGY
|
|
440
440
|
reconcile/test/test_terraform_cloudflare_resources.py,sha256=NK_uktyWihkQ3gMN4bCaKerpi43CXAVYGIKTfcz05rY,13550
|
441
441
|
reconcile/test/test_terraform_cloudflare_users.py,sha256=RAFtMMdqZha3jNnNNsqbNQQUDSqUzdoM63rCw7fs4Fo,27456
|
442
442
|
reconcile/test/test_terraform_repo.py,sha256=soKFJfF8tWIimDs39RQl3Hnh-Od-bR4PfnEA2s1UprM,11552
|
443
|
-
reconcile/test/test_terraform_resources.py,sha256=
|
443
|
+
reconcile/test/test_terraform_resources.py,sha256=O8kCYxGKqILbbCP86eJ3ESjatdPa1m3wQYXxrVfc_eo,15082
|
444
444
|
reconcile/test/test_terraform_tgw_attachments.py,sha256=cAq6exc-K-jtLla1CZUZQzVnBkyDnIlL7jybnddhLKc,36861
|
445
445
|
reconcile/test/test_terraform_users.py,sha256=Xn4y6EcxnNQb6XcPoOhz_Ikxmh9Nrsu88OM1scN9hzY,5434
|
446
446
|
reconcile/test/test_terraform_vpc_peerings.py,sha256=ubcsKh0TrUIwuI1-W3ETIgzsFvzAyeoFmEJFC-IK6JY,20538
|
@@ -524,13 +524,14 @@ reconcile/utils/defer.py,sha256=SniUsbgOEs9Pa8JkecLu0F94O63yQPByKXaElDYe0FI,377
|
|
524
524
|
reconcile/utils/differ.py,sha256=kJmUp9ZffFPSUEviaAw3s9c92ErwRJeHaRexGPai7wA,7643
|
525
525
|
reconcile/utils/disabled_integrations.py,sha256=avdDsFyl_LdTsrPVzlcIhWzT_V4C4MXw1ZC__aOtluE,1126
|
526
526
|
reconcile/utils/dnsutils.py,sha256=VX4gDQXpiMYVuT0pvNbzzSgfqmsWOM2qtjNQHUyIYF8,370
|
527
|
-
reconcile/utils/early_exit_cache.py,sha256=
|
527
|
+
reconcile/utils/early_exit_cache.py,sha256=2uaiYIILA64D8mqqPj-GSvVBa80bN5XQRKz9x33iIpc,2304
|
528
528
|
reconcile/utils/elasticsearch_exceptions.py,sha256=UY5Z3y2hw7T73sPJ6dHmUybegiIophrKFdTfdsOa6UY,379
|
529
529
|
reconcile/utils/environ.py,sha256=VnW3zp6Un_UJn5BU4FU8RfhuqtZp0s-VeuuHnqC_WcQ,515
|
530
530
|
reconcile/utils/exceptions.py,sha256=DwfnWUpVOotpP79RWZ2pycmG6nKCL00RBIeZLYkQPW4,635
|
531
531
|
reconcile/utils/expiration.py,sha256=BXwKE50sNIV-Lszke97fxitNkLxYszoOLW1LBgp_yqg,1246
|
532
|
+
reconcile/utils/extended_early_exit.py,sha256=gLWRtgzRB584iX4pVfGjfZxbIDd_AQPYu8MkQkReA3U,5688
|
532
533
|
reconcile/utils/external_resource_spec.py,sha256=OGPKH3IKXgJszRTgE5U_QKgU-s4BHQnx97Lj-Krz46k,6655
|
533
|
-
reconcile/utils/external_resources.py,sha256=
|
534
|
+
reconcile/utils/external_resources.py,sha256=a2CkJ3KLociYBnc_9F2VWfZGWMhzDl6fDNhwo2U-MWU,7501
|
534
535
|
reconcile/utils/filtering.py,sha256=zZnHH0u0SaTDyzuFXZ_mREURGLvjEqQIQy4z-7QBVlc,419
|
535
536
|
reconcile/utils/git.py,sha256=Qad7mfPuS9s7eKODeWSewehwSGgJPCbQuLda1qg_6GA,1522
|
536
537
|
reconcile/utils/git_secrets.py,sha256=0wGNL5mvDtVPRuu3vEQgld1Am64gIDJHtmu1_ZKxMAI,1973
|
@@ -553,7 +554,7 @@ reconcile/utils/keycloak.py,sha256=UqOsAcHKmmIunroWB5YzC1fUZ3S3aq6L7trn6vLRmXY,3
|
|
553
554
|
reconcile/utils/ldap_client.py,sha256=ho4veSrHqQWs0YhLFyKeD-duCwY8Nc5gUIA5qLENuMY,2502
|
554
555
|
reconcile/utils/lean_terraform_client.py,sha256=zReyNPJbr2uOdrdh8Qfe-OZQBoRwxb5Za_ddeoUCYVk,4064
|
555
556
|
reconcile/utils/make.py,sha256=QaEwucrzbl8-VHS66Wfdjfo0ubmAcvt_hZGpiGsKU50,231
|
556
|
-
reconcile/utils/metrics.py,sha256=
|
557
|
+
reconcile/utils/metrics.py,sha256=7nXdctmZ0UtGMHPpS3V55sfH4xpMPqdYaJ3JKAUc_sM,18474
|
557
558
|
reconcile/utils/models.py,sha256=R3wF68yiaT0H2sQVMmHhHSTGI3JKKYEVjoizhHkySn8,4533
|
558
559
|
reconcile/utils/oc.py,sha256=iAoM7zSm72zBOmCNW19ttAvm76tUgS26vlpb8SvVYwU,64058
|
559
560
|
reconcile/utils/oc_connection_parameters.py,sha256=85slrnDigYwYmzhyceVkMElWzFArp4ge1d-fHXVqh0w,9729
|
@@ -580,8 +581,8 @@ reconcile/utils/sqs_gateway.py,sha256=gFl9DM4DmGnptuxTOe4lS3YTyE80eSAvK42ljS8h4d
|
|
580
581
|
reconcile/utils/state.py,sha256=SAa6QLHu9lr0yqLCBy2AypNx1IPCJWlrRBrvlzAKsOU,14505
|
581
582
|
reconcile/utils/structs.py,sha256=LcbLEg8WxfRqM6nW7NhcWN0YeqF7SQzxOgntmLs1SgY,352
|
582
583
|
reconcile/utils/template.py,sha256=wTvRU4AnAV_o042tD4Mwls2dwWMuk7MKnde3MaCjaYg,331
|
583
|
-
reconcile/utils/terraform_client.py,sha256=
|
584
|
-
reconcile/utils/terrascript_aws_client.py,sha256=
|
584
|
+
reconcile/utils/terraform_client.py,sha256=_jBriLBwU005bDxWlq7CRByOkVCfiH47oBzB0ArNAY8,31901
|
585
|
+
reconcile/utils/terrascript_aws_client.py,sha256=Ht2akaR4bRERLoyN_Zh2JBbN1-p-ofXICqW-oXzGcFk,265789
|
585
586
|
reconcile/utils/three_way_diff_strategy.py,sha256=nyqeQsLCoPI6e16k2CF3b9KNgQLU-rPf5RtfdUfVMwE,4468
|
586
587
|
reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM,344
|
587
588
|
reconcile/utils/unleash.py,sha256=1D56CsZfE3ShDtN3IErE1T2eeIwNmxhK-yYbCotJ99E,3601
|
@@ -657,7 +658,7 @@ tools/app_interface_metrics_exporter.py,sha256=zkwkxdAUAxjdc-pzx2_oJXG25fo0Fnyd5
|
|
657
658
|
tools/app_interface_reporter.py,sha256=upA-J-n-HXHKVDINRuMR7vTt-iJvQORKUVi9D3leQto,17738
|
658
659
|
tools/glitchtip_access_reporter.py,sha256=oPBnk_YoDuljU3v0FaChzOwwnk4vap1xEE67QEjzdqs,2948
|
659
660
|
tools/glitchtip_access_revalidation.py,sha256=8kbBJk04mkq28kWoRDDkfCGIF3GRg3pJrFAh1sW0dbk,2821
|
660
|
-
tools/qontract_cli.py,sha256=
|
661
|
+
tools/qontract_cli.py,sha256=mN5oXM_lmFmCGwoGU77qqO8nzlfKnNGlP6y-ILFyHIQ,103423
|
661
662
|
tools/sd_app_sre_alert_report.py,sha256=e9vAdyenUz2f5c8-z-5WY0wv-SJ9aePKDH2r4IwB6pc,5063
|
662
663
|
tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
663
664
|
tools/cli_commands/gpg_encrypt.py,sha256=w8hl4jIEWk5wKbEFN6fVEOwUJGmdlvOqYodW3XSN7mU,4978
|
@@ -665,11 +666,11 @@ tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOy
|
|
665
666
|
tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
|
666
667
|
tools/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
667
668
|
tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvftCWEEf-g1mfXOtgCog-g,1271
|
668
|
-
tools/test/test_qontract_cli.py,sha256=
|
669
|
+
tools/test/test_qontract_cli.py,sha256=se-YG_YVCWRFrnCPvBVHDBT_59CkbIoEni-4SJa8_MU,2755
|
669
670
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
670
671
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
671
|
-
qontract_reconcile-0.10.
|
672
|
-
qontract_reconcile-0.10.
|
673
|
-
qontract_reconcile-0.10.
|
674
|
-
qontract_reconcile-0.10.
|
675
|
-
qontract_reconcile-0.10.
|
672
|
+
qontract_reconcile-0.10.1rc538.dist-info/METADATA,sha256=TkmlNiYqcISDkpP1_R0KR4m709lpVpeeJUAkfjVEFVc,2349
|
673
|
+
qontract_reconcile-0.10.1rc538.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
674
|
+
qontract_reconcile-0.10.1rc538.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
|
675
|
+
qontract_reconcile-0.10.1rc538.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
676
|
+
qontract_reconcile-0.10.1rc538.dist-info/RECORD,,
|
reconcile/cli.py
CHANGED
@@ -507,6 +507,30 @@ def trigger_integration(function):
|
|
507
507
|
return function
|
508
508
|
|
509
509
|
|
510
|
+
def enable_extended_early_exit(function):
|
511
|
+
return click.option(
|
512
|
+
"--enable-extended-early-exit/--no-enable-extended-early-exit",
|
513
|
+
default=False,
|
514
|
+
help="enable extended early exit.",
|
515
|
+
)(function)
|
516
|
+
|
517
|
+
|
518
|
+
def extended_early_exit_cache_ttl_seconds(function):
|
519
|
+
return click.option(
|
520
|
+
"--extended-early-exit-cache-ttl-seconds",
|
521
|
+
default=3600,
|
522
|
+
help="TTL of extended early exit cache in seconds.",
|
523
|
+
)(function)
|
524
|
+
|
525
|
+
|
526
|
+
def log_cached_log_output(function):
|
527
|
+
return click.option(
|
528
|
+
"--log-cached-log-output/--no-log-cached-log-output",
|
529
|
+
default=False,
|
530
|
+
help="log the cached log output.",
|
531
|
+
)(function)
|
532
|
+
|
533
|
+
|
510
534
|
def register_faulthandler(fileobj=sys.__stderr__):
|
511
535
|
if fileobj:
|
512
536
|
if not faulthandler.is_enabled():
|
@@ -1738,6 +1762,9 @@ def terraform_repo(ctx, output_file, gitlab_project_id, gitlab_merge_request_id)
|
|
1738
1762
|
@enable_deletion(default=False)
|
1739
1763
|
@account_name_multiple
|
1740
1764
|
@exclude_aws_accounts
|
1765
|
+
@enable_extended_early_exit
|
1766
|
+
@extended_early_exit_cache_ttl_seconds
|
1767
|
+
@log_cached_log_output
|
1741
1768
|
@click.option(
|
1742
1769
|
"--light/--full",
|
1743
1770
|
default=False,
|
@@ -1755,6 +1782,9 @@ def terraform_resources(
|
|
1755
1782
|
vault_output_path,
|
1756
1783
|
account_name,
|
1757
1784
|
exclude_accounts,
|
1785
|
+
enable_extended_early_exit,
|
1786
|
+
extended_early_exit_cache_ttl_seconds,
|
1787
|
+
log_cached_log_output,
|
1758
1788
|
):
|
1759
1789
|
import reconcile.terraform_resources
|
1760
1790
|
|
@@ -1772,6 +1802,9 @@ def terraform_resources(
|
|
1772
1802
|
vault_output_path,
|
1773
1803
|
account_name=account_name,
|
1774
1804
|
exclude_accounts=exclude_accounts,
|
1805
|
+
enable_extended_early_exit=enable_extended_early_exit,
|
1806
|
+
extended_early_exit_cache_ttl_seconds=extended_early_exit_cache_ttl_seconds,
|
1807
|
+
log_cached_log_output=log_cached_log_output,
|
1775
1808
|
)
|
1776
1809
|
|
1777
1810
|
|
reconcile/terraform_resources.py
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
import logging
|
2
|
-
import shutil
|
3
|
-
import sys
|
4
2
|
from collections.abc import (
|
5
3
|
Callable,
|
6
4
|
Iterable,
|
@@ -11,6 +9,7 @@ from typing import (
|
|
11
9
|
Collection,
|
12
10
|
Optional,
|
13
11
|
Sequence,
|
12
|
+
TypedDict,
|
14
13
|
cast,
|
15
14
|
)
|
16
15
|
|
@@ -33,6 +32,10 @@ from reconcile.typed_queries.terraform_namespaces import get_namespaces
|
|
33
32
|
from reconcile.utils import gql
|
34
33
|
from reconcile.utils.aws_api import AWSApi
|
35
34
|
from reconcile.utils.defer import defer
|
35
|
+
from reconcile.utils.extended_early_exit import (
|
36
|
+
ExtendedEarlyExitRunnerResult,
|
37
|
+
extended_early_exit_run,
|
38
|
+
)
|
36
39
|
from reconcile.utils.external_resource_spec import (
|
37
40
|
ExternalResourceSpec,
|
38
41
|
ExternalResourceSpecInventory,
|
@@ -52,10 +55,12 @@ from reconcile.utils.ocm import OCMMap
|
|
52
55
|
from reconcile.utils.openshift_resource import OpenshiftResource as OR
|
53
56
|
from reconcile.utils.openshift_resource import ResourceInventory
|
54
57
|
from reconcile.utils.runtime.integration import DesiredStateShardConfig
|
55
|
-
from reconcile.utils.secret_reader import create_secret_reader
|
58
|
+
from reconcile.utils.secret_reader import SecretReaderBase, create_secret_reader
|
56
59
|
from reconcile.utils.semver_helper import make_semver
|
57
60
|
from reconcile.utils.terraform_client import TerraformClient as Terraform
|
61
|
+
from reconcile.utils.terrascript_aws_client import TerrascriptClient
|
58
62
|
from reconcile.utils.terrascript_aws_client import TerrascriptClient as Terrascript
|
63
|
+
from reconcile.utils.unleash import get_feature_toggle_state
|
59
64
|
from reconcile.utils.vault import (
|
60
65
|
VaultClient,
|
61
66
|
_VaultClient,
|
@@ -117,18 +122,14 @@ def populate_oc_resources(
|
|
117
122
|
|
118
123
|
|
119
124
|
def fetch_current_state(
|
120
|
-
dry_run: bool,
|
121
125
|
namespaces: Iterable[NamespaceV1],
|
122
126
|
thread_pool_size: int,
|
123
127
|
internal: Optional[bool],
|
124
128
|
use_jump_host: bool,
|
125
129
|
account_names: Optional[Iterable[str]],
|
126
|
-
|
130
|
+
secret_reader: SecretReaderBase,
|
131
|
+
) -> tuple[ResourceInventory, OCMap]:
|
127
132
|
ri = ResourceInventory()
|
128
|
-
if dry_run:
|
129
|
-
return ri, None
|
130
|
-
vault_settings = get_app_interface_vault_settings()
|
131
|
-
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
132
133
|
oc_map = init_oc_map_from_namespaces(
|
133
134
|
namespaces=namespaces,
|
134
135
|
integration=QONTRACT_INTEGRATION,
|
@@ -172,64 +173,74 @@ def init_working_dirs(
|
|
172
173
|
|
173
174
|
|
174
175
|
def filter_accounts_by_name(
|
175
|
-
accounts: Iterable[
|
176
|
-
) ->
|
177
|
-
return [ac for ac in accounts if ac["name"] in
|
176
|
+
accounts: Iterable[dict[str, Any]], names: Iterable[str]
|
177
|
+
) -> list[dict[str, Any]]:
|
178
|
+
return [ac for ac in accounts if ac["name"] in names]
|
178
179
|
|
179
180
|
|
180
181
|
def exclude_accounts_by_name(
|
181
|
-
accounts: Iterable[
|
182
|
-
) ->
|
183
|
-
return [ac for ac in accounts if ac["name"] not in
|
182
|
+
accounts: Iterable[dict[str, Any]], names: Iterable[str]
|
183
|
+
) -> list[dict[str, Any]]:
|
184
|
+
return [ac for ac in accounts if ac["name"] not in names]
|
184
185
|
|
185
186
|
|
186
187
|
def validate_account_names(
|
187
188
|
accounts: Collection[Mapping[str, Any]], names: Collection[str]
|
188
189
|
) -> None:
|
189
|
-
if
|
190
|
-
missing_names = set(names) - {a["name"] for a in accounts}
|
190
|
+
if missing_names := set(names) - {a["name"] for a in accounts}:
|
191
191
|
raise ValueError(
|
192
192
|
f"Accounts {missing_names} were provided as arguments, but not found in app-interface. Check your input for typos or for missing AWS account definitions."
|
193
193
|
)
|
194
194
|
|
195
195
|
|
196
|
-
def
|
196
|
+
def get_aws_accounts(
|
197
197
|
dry_run: bool,
|
198
|
-
print_to_file: Optional[str],
|
199
|
-
thread_pool_size: int,
|
200
|
-
internal: Optional[bool],
|
201
|
-
use_jump_host: bool,
|
202
198
|
include_accounts: Optional[Collection[str]],
|
203
199
|
exclude_accounts: Optional[Collection[str]],
|
204
|
-
) ->
|
205
|
-
|
206
|
-
|
200
|
+
) -> list[dict[str, Any]]:
|
201
|
+
if exclude_accounts and not dry_run:
|
202
|
+
message = "--exclude-accounts is only supported in dry-run mode"
|
203
|
+
logging.error(message)
|
204
|
+
raise ExcludeAccountsAndDryRunException(message)
|
205
|
+
|
206
|
+
if exclude_accounts and include_accounts:
|
207
|
+
message = "Using --exclude-accounts and --account-name at the same time is not allowed"
|
208
|
+
logging.error(message)
|
209
|
+
raise ExcludeAccountsAndAccountNameException(message)
|
210
|
+
|
211
|
+
# If we are not running in dry run we don't want to run with more than one account
|
212
|
+
if include_accounts and len(include_accounts) > 1 and not dry_run:
|
213
|
+
message = "Running with multiple accounts is only supported in dry-run mode"
|
214
|
+
logging.error(message)
|
215
|
+
raise MultipleAccountNamesInDryRunException(message)
|
216
|
+
|
207
217
|
accounts = queries.get_aws_accounts(terraform_state=True)
|
208
|
-
|
209
|
-
|
210
|
-
validate_account_names(
|
211
|
-
|
212
|
-
if
|
218
|
+
|
219
|
+
if exclude_accounts:
|
220
|
+
validate_account_names(accounts, exclude_accounts)
|
221
|
+
filtered_accounts = exclude_accounts_by_name(accounts, exclude_accounts)
|
222
|
+
if not filtered_accounts:
|
213
223
|
raise ValueError("You have excluded all aws accounts, verify your input")
|
214
|
-
|
215
|
-
|
216
|
-
|
224
|
+
return filtered_accounts
|
225
|
+
|
226
|
+
if include_accounts:
|
217
227
|
validate_account_names(accounts, include_accounts)
|
218
|
-
|
219
|
-
settings = queries.get_app_interface_settings()
|
228
|
+
return filter_accounts_by_name(accounts, include_accounts)
|
220
229
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
)
|
230
|
+
return accounts
|
231
|
+
|
232
|
+
|
233
|
+
def setup(
|
234
|
+
accounts: list[dict[str, Any]],
|
235
|
+
account_names: set[str],
|
236
|
+
tf_namespaces: list[NamespaceV1],
|
237
|
+
print_to_file: Optional[str],
|
238
|
+
thread_pool_size: int,
|
239
|
+
) -> tuple[Terraform, TerrascriptClient, SecretReaderBase]:
|
240
|
+
vault_settings = get_app_interface_vault_settings()
|
241
|
+
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
232
242
|
|
243
|
+
settings = queries.get_app_interface_settings()
|
233
244
|
# initialize terrascript (scripting engine to generate terraform manifests)
|
234
245
|
ts, working_dirs = init_working_dirs(accounts, thread_pool_size, settings=settings)
|
235
246
|
|
@@ -260,7 +271,7 @@ def setup(
|
|
260
271
|
ts.populate_resources(ocm_map=ocm_map)
|
261
272
|
ts.dump(print_to_file, existing_dirs=working_dirs)
|
262
273
|
|
263
|
-
return
|
274
|
+
return tf, ts, secret_reader
|
264
275
|
|
265
276
|
|
266
277
|
def filter_tf_namespaces(
|
@@ -290,21 +301,6 @@ def filter_tf_namespaces(
|
|
290
301
|
return tf_namespaces
|
291
302
|
|
292
303
|
|
293
|
-
def cleanup_and_exit(
|
294
|
-
tf: Optional[Terraform] = None,
|
295
|
-
status: bool = False,
|
296
|
-
working_dirs: Optional[Mapping[str, str]] = None,
|
297
|
-
) -> None:
|
298
|
-
if working_dirs is None:
|
299
|
-
working_dirs = {}
|
300
|
-
if tf is None:
|
301
|
-
for wd in working_dirs.values():
|
302
|
-
shutil.rmtree(wd)
|
303
|
-
else:
|
304
|
-
tf.cleanup()
|
305
|
-
sys.exit(status)
|
306
|
-
|
307
|
-
|
308
304
|
@retry()
|
309
305
|
def write_outputs_to_vault(
|
310
306
|
vault_path: str, resource_specs: ExternalResourceSpecInventory
|
@@ -366,65 +362,125 @@ def run(
|
|
366
362
|
vault_output_path: str = "",
|
367
363
|
account_name: Optional[Sequence[str]] = None,
|
368
364
|
exclude_accounts: Optional[Sequence[str]] = None,
|
365
|
+
enable_extended_early_exit: bool = False,
|
366
|
+
extended_early_exit_cache_ttl_seconds: int = 3600,
|
367
|
+
log_cached_log_output: bool = False,
|
369
368
|
defer: Optional[Callable] = None,
|
370
369
|
) -> None:
|
371
|
-
if exclude_accounts and not dry_run:
|
372
|
-
message = "--exclude-accounts is only supported in dry-run mode"
|
373
|
-
logging.error(message)
|
374
|
-
raise ExcludeAccountsAndDryRunException(message)
|
375
|
-
|
376
|
-
if exclude_accounts and account_name:
|
377
|
-
message = "Using --exclude-accounts and --account-name at the same time is not allowed"
|
378
|
-
logging.error(message)
|
379
|
-
raise ExcludeAccountsAndAccountNameException(message)
|
380
|
-
|
381
370
|
# account_name is a tuple of account names for more detail go to
|
382
371
|
# https://click.palletsprojects.com/en/8.1.x/options/#multiple-options
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
logging.error(message)
|
392
|
-
raise MultipleAccountNamesInDryRunException(message)
|
372
|
+
accounts = get_aws_accounts(dry_run, account_name, exclude_accounts)
|
373
|
+
account_names = {a["name"] for a in accounts}
|
374
|
+
tf_namespaces = get_tf_namespaces(account_names)
|
375
|
+
if not tf_namespaces:
|
376
|
+
logging.warning(
|
377
|
+
"No terraform namespaces found, consider disabling this integration, account names: "
|
378
|
+
f"{', '.join(account_names)}"
|
379
|
+
)
|
393
380
|
|
394
|
-
|
395
|
-
|
381
|
+
tf, ts, secret_reader = setup(
|
382
|
+
accounts,
|
383
|
+
account_names,
|
384
|
+
tf_namespaces,
|
396
385
|
print_to_file,
|
397
386
|
thread_pool_size,
|
398
|
-
internal,
|
399
|
-
use_jump_host,
|
400
|
-
account_names,
|
401
|
-
exclude_accounts,
|
402
387
|
)
|
403
|
-
|
388
|
+
if defer:
|
389
|
+
defer(tf.cleanup)
|
404
390
|
|
405
|
-
|
406
|
-
defer(oc_map.cleanup)
|
391
|
+
publish_metrics(ts.resource_spec_inventory, QONTRACT_INTEGRATION)
|
407
392
|
|
408
393
|
if print_to_file:
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
394
|
+
return
|
395
|
+
|
396
|
+
runner_params: RunnerParams = dict(
|
397
|
+
accounts=accounts,
|
398
|
+
account_names=account_names,
|
399
|
+
tf_namespaces=tf_namespaces,
|
400
|
+
tf=tf,
|
401
|
+
ts=ts,
|
402
|
+
secret_reader=secret_reader,
|
403
|
+
dry_run=dry_run,
|
404
|
+
enable_deletion=enable_deletion,
|
405
|
+
thread_pool_size=thread_pool_size,
|
406
|
+
internal=internal,
|
407
|
+
use_jump_host=use_jump_host,
|
408
|
+
light=light,
|
409
|
+
vault_output_path=vault_output_path,
|
410
|
+
defer=defer,
|
411
|
+
)
|
413
412
|
|
413
|
+
if enable_extended_early_exit and get_feature_toggle_state(
|
414
|
+
"terraform-resources-extended-early-exit",
|
415
|
+
default=False,
|
416
|
+
):
|
417
|
+
extended_early_exit_run(
|
418
|
+
integration=QONTRACT_INTEGRATION,
|
419
|
+
integration_version=QONTRACT_INTEGRATION_VERSION,
|
420
|
+
dry_run=dry_run,
|
421
|
+
cache_source=ts.terraform_configurations(),
|
422
|
+
ttl_seconds=extended_early_exit_cache_ttl_seconds,
|
423
|
+
logger=logging.getLogger(),
|
424
|
+
runner=runner,
|
425
|
+
runner_params=runner_params,
|
426
|
+
secret_reader=secret_reader,
|
427
|
+
log_cached_log_output=log_cached_log_output,
|
428
|
+
)
|
429
|
+
else:
|
430
|
+
runner(**runner_params)
|
431
|
+
|
432
|
+
|
433
|
+
class RunnerParams(TypedDict):
|
434
|
+
accounts: list[dict[str, Any]]
|
435
|
+
account_names: set[str]
|
436
|
+
tf_namespaces: list[NamespaceV1]
|
437
|
+
tf: Terraform
|
438
|
+
ts: Terrascript
|
439
|
+
secret_reader: SecretReaderBase
|
440
|
+
dry_run: bool
|
441
|
+
enable_deletion: bool
|
442
|
+
thread_pool_size: int
|
443
|
+
internal: Optional[bool]
|
444
|
+
use_jump_host: bool
|
445
|
+
light: bool
|
446
|
+
vault_output_path: str
|
447
|
+
defer: Optional[Callable]
|
448
|
+
|
449
|
+
|
450
|
+
def runner(
|
451
|
+
accounts: list[dict[str, Any]],
|
452
|
+
account_names: set[str],
|
453
|
+
tf_namespaces: list[NamespaceV1],
|
454
|
+
tf: Terraform,
|
455
|
+
ts: Terrascript,
|
456
|
+
secret_reader: SecretReaderBase,
|
457
|
+
dry_run: bool,
|
458
|
+
enable_deletion: bool = False,
|
459
|
+
thread_pool_size: int = 10,
|
460
|
+
internal: Optional[bool] = None,
|
461
|
+
use_jump_host: bool = True,
|
462
|
+
light: bool = False,
|
463
|
+
vault_output_path: str = "",
|
464
|
+
defer: Optional[Callable] = None,
|
465
|
+
) -> ExtendedEarlyExitRunnerResult:
|
414
466
|
if not light:
|
415
467
|
disabled_deletions_detected, err = tf.plan(enable_deletion)
|
416
468
|
if err:
|
417
|
-
|
469
|
+
raise RuntimeError("Terraform plan has errors")
|
418
470
|
if disabled_deletions_detected:
|
419
|
-
|
471
|
+
raise RuntimeError("Terraform plan has disabled deletions detected")
|
420
472
|
|
421
473
|
if dry_run:
|
422
|
-
|
474
|
+
return ExtendedEarlyExitRunnerResult(
|
475
|
+
payload=ts.terraform_configurations(),
|
476
|
+
applied_count=0,
|
477
|
+
)
|
423
478
|
|
424
|
-
if
|
479
|
+
acc_name = accounts[0]["name"] if accounts else None
|
480
|
+
if not light and tf.should_apply():
|
425
481
|
err = tf.apply()
|
426
482
|
if err:
|
427
|
-
|
483
|
+
raise RuntimeError("Terraform apply has errors")
|
428
484
|
|
429
485
|
if defer:
|
430
486
|
defer(
|
@@ -437,26 +493,41 @@ def run(
|
|
437
493
|
|
438
494
|
# refresh output data after terraform apply
|
439
495
|
tf.populate_terraform_output_secrets(
|
440
|
-
resource_specs=
|
496
|
+
resource_specs=ts.resource_spec_inventory, init_rds_replica_source=True
|
441
497
|
)
|
498
|
+
|
499
|
+
ri, oc_map = fetch_current_state(
|
500
|
+
tf_namespaces,
|
501
|
+
thread_pool_size,
|
502
|
+
internal,
|
503
|
+
use_jump_host,
|
504
|
+
account_names,
|
505
|
+
secret_reader=secret_reader,
|
506
|
+
)
|
507
|
+
if defer:
|
508
|
+
defer(oc_map.cleanup)
|
442
509
|
# populate the resource inventory with latest output data
|
443
|
-
populate_desired_state(ri,
|
510
|
+
populate_desired_state(ri, ts.resource_spec_inventory)
|
444
511
|
|
445
512
|
ob.publish_metrics(ri, QONTRACT_INTEGRATION)
|
446
|
-
actions =
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
513
|
+
actions = ob.realize_data(
|
514
|
+
dry_run,
|
515
|
+
oc_map,
|
516
|
+
ri,
|
517
|
+
thread_pool_size,
|
518
|
+
caller=acc_name,
|
519
|
+
)
|
451
520
|
|
452
521
|
if actions and vault_output_path:
|
453
|
-
write_outputs_to_vault(vault_output_path,
|
522
|
+
write_outputs_to_vault(vault_output_path, ts.resource_spec_inventory)
|
454
523
|
|
455
524
|
if ri.has_error_registered():
|
456
|
-
|
457
|
-
cleanup_and_exit(tf, err)
|
525
|
+
raise RuntimeError("Resource inventory has errors registered")
|
458
526
|
|
459
|
-
|
527
|
+
return ExtendedEarlyExitRunnerResult(
|
528
|
+
payload=ts.terraform_configurations(),
|
529
|
+
applied_count=tf.apply_count + len(actions),
|
530
|
+
)
|
460
531
|
|
461
532
|
|
462
533
|
def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
@@ -4,6 +4,7 @@ from collections.abc import (
|
|
4
4
|
Mapping,
|
5
5
|
)
|
6
6
|
from typing import Any
|
7
|
+
from unittest.mock import MagicMock, create_autospec
|
7
8
|
|
8
9
|
import pytest
|
9
10
|
from pytest_mock import MockerFixture
|
@@ -12,6 +13,7 @@ import reconcile.terraform_resources as integ
|
|
12
13
|
from reconcile.gql_definitions.terraform_resources.terraform_resources_namespaces import (
|
13
14
|
NamespaceV1,
|
14
15
|
)
|
16
|
+
from reconcile.utils.secret_reader import SecretReaderBase
|
15
17
|
|
16
18
|
|
17
19
|
def test_cannot_use_exclude_accounts_if_not_dry_run():
|
@@ -71,7 +73,7 @@ def test_cannot_pass_two_aws_account_if_not_dry_run():
|
|
71
73
|
def test_filter_accounts_by_name():
|
72
74
|
accounts = [{"name": "a"}, {"name": "b"}, {"name": "c"}]
|
73
75
|
|
74
|
-
filtered = integ.filter_accounts_by_name(accounts,
|
76
|
+
filtered = integ.filter_accounts_by_name(accounts, names=("a", "b"))
|
75
77
|
|
76
78
|
assert filtered == [{"name": "a"}, {"name": "b"}]
|
77
79
|
|
@@ -79,7 +81,7 @@ def test_filter_accounts_by_name():
|
|
79
81
|
def test_exclude_accounts_by_name():
|
80
82
|
accounts = [{"name": "a"}, {"name": "b"}, {"name": "c"}]
|
81
83
|
|
82
|
-
filtered = integ.exclude_accounts_by_name(accounts,
|
84
|
+
filtered = integ.exclude_accounts_by_name(accounts, names=("a", "b"))
|
83
85
|
|
84
86
|
assert filtered == [{"name": "c"}]
|
85
87
|
|
@@ -258,27 +260,255 @@ def test_filter_tf_namespaces_namespace_deleted(gql_class_factory: Callable):
|
|
258
260
|
assert filtered == [ns2]
|
259
261
|
|
260
262
|
|
261
|
-
def
|
263
|
+
def setup_mocks(
|
264
|
+
mocker: MockerFixture,
|
265
|
+
secret_reader: SecretReaderBase,
|
266
|
+
aws_accounts: list[dict[str, Any]],
|
267
|
+
tf_namespaces: list[NamespaceV1],
|
268
|
+
feature_toggle_state: bool = True,
|
269
|
+
) -> dict[str, Any]:
|
262
270
|
mocked_queries = mocker.patch("reconcile.terraform_resources.queries")
|
263
|
-
mocked_queries.get_aws_accounts.return_value =
|
271
|
+
mocked_queries.get_aws_accounts.return_value = aws_accounts
|
264
272
|
mocked_queries.get_app_interface_settings.return_value = []
|
265
273
|
|
266
|
-
mocker.patch(
|
274
|
+
mocker.patch(
|
275
|
+
"reconcile.terraform_resources.get_namespaces"
|
276
|
+
).return_value = tf_namespaces
|
267
277
|
|
268
|
-
mocked_ts = mocker.patch(
|
269
|
-
|
278
|
+
mocked_ts = mocker.patch(
|
279
|
+
"reconcile.terraform_resources.Terrascript", autospec=True
|
280
|
+
).return_value
|
281
|
+
mocked_ts.resource_spec_inventory = {}
|
270
282
|
|
271
|
-
mocked_tf = mocker.patch(
|
272
|
-
|
273
|
-
|
283
|
+
mocked_tf = mocker.patch(
|
284
|
+
"reconcile.terraform_resources.Terraform", autospec=True
|
285
|
+
).return_value
|
286
|
+
mocked_tf.plan.return_value = (False, None)
|
287
|
+
mocked_tf.should_apply.return_value = False
|
274
288
|
|
275
289
|
mocker.patch("reconcile.terraform_resources.AWSApi", autospec=True)
|
276
|
-
mocker.patch("reconcile.terraform_resources.sys")
|
277
290
|
|
278
291
|
mocked_logging = mocker.patch("reconcile.terraform_resources.logging")
|
279
292
|
|
293
|
+
mocker.patch("reconcile.terraform_resources.get_app_interface_vault_settings")
|
294
|
+
|
295
|
+
mocker.patch(
|
296
|
+
"reconcile.terraform_resources.create_secret_reader",
|
297
|
+
return_value=secret_reader,
|
298
|
+
)
|
299
|
+
|
300
|
+
mock_extended_early_exit_run = mocker.patch(
|
301
|
+
"reconcile.terraform_resources.extended_early_exit_run"
|
302
|
+
)
|
303
|
+
|
304
|
+
get_feature_toggle_state = mocker.patch(
|
305
|
+
"reconcile.terraform_resources.get_feature_toggle_state",
|
306
|
+
return_value=feature_toggle_state,
|
307
|
+
)
|
308
|
+
|
309
|
+
return {
|
310
|
+
"queries": mocked_queries,
|
311
|
+
"ts": mocked_ts,
|
312
|
+
"tf": mocked_tf,
|
313
|
+
"logging": mocked_logging,
|
314
|
+
"extended_early_exit_run": mock_extended_early_exit_run,
|
315
|
+
"get_feature_toggle_state": get_feature_toggle_state,
|
316
|
+
}
|
317
|
+
|
318
|
+
|
319
|
+
def test_empty_run(
|
320
|
+
mocker: MockerFixture,
|
321
|
+
secret_reader: SecretReaderBase,
|
322
|
+
) -> None:
|
323
|
+
mocks = setup_mocks(
|
324
|
+
mocker,
|
325
|
+
secret_reader,
|
326
|
+
aws_accounts=[{"name": "a"}],
|
327
|
+
tf_namespaces=[],
|
328
|
+
)
|
329
|
+
|
280
330
|
integ.run(True, account_name="a")
|
281
331
|
|
282
|
-
|
332
|
+
mocks["logging"].warning.assert_called_once_with(
|
283
333
|
"No terraform namespaces found, consider disabling this integration, account names: a"
|
284
334
|
)
|
335
|
+
|
336
|
+
|
337
|
+
def test_run_with_extended_early_exit_run_enabled(
|
338
|
+
mocker: MockerFixture,
|
339
|
+
secret_reader: SecretReaderBase,
|
340
|
+
) -> None:
|
341
|
+
mocks = setup_mocks(
|
342
|
+
mocker,
|
343
|
+
secret_reader,
|
344
|
+
aws_accounts=[{"name": "a"}],
|
345
|
+
tf_namespaces=[],
|
346
|
+
)
|
347
|
+
defer = MagicMock()
|
348
|
+
expected_runner_params = integ.RunnerParams(
|
349
|
+
accounts=[{"name": "a"}],
|
350
|
+
account_names={"a"},
|
351
|
+
tf_namespaces=[],
|
352
|
+
tf=mocks["tf"],
|
353
|
+
ts=mocks["ts"],
|
354
|
+
secret_reader=secret_reader,
|
355
|
+
dry_run=True,
|
356
|
+
enable_deletion=False,
|
357
|
+
thread_pool_size=10,
|
358
|
+
internal=None,
|
359
|
+
use_jump_host=True,
|
360
|
+
light=False,
|
361
|
+
vault_output_path="",
|
362
|
+
defer=defer,
|
363
|
+
)
|
364
|
+
|
365
|
+
integ.run.__wrapped__(
|
366
|
+
True,
|
367
|
+
account_name="a",
|
368
|
+
enable_extended_early_exit=True,
|
369
|
+
extended_early_exit_cache_ttl_seconds=60,
|
370
|
+
log_cached_log_output=True,
|
371
|
+
defer=defer,
|
372
|
+
)
|
373
|
+
|
374
|
+
mocks["extended_early_exit_run"].assert_called_once_with(
|
375
|
+
integration=integ.QONTRACT_INTEGRATION,
|
376
|
+
integration_version=integ.QONTRACT_INTEGRATION_VERSION,
|
377
|
+
dry_run=True,
|
378
|
+
cache_source=mocks["ts"].terraform_configurations.return_value,
|
379
|
+
ttl_seconds=60,
|
380
|
+
logger=mocks["logging"].getLogger.return_value,
|
381
|
+
runner=integ.runner,
|
382
|
+
runner_params=expected_runner_params,
|
383
|
+
secret_reader=secret_reader,
|
384
|
+
log_cached_log_output=True,
|
385
|
+
)
|
386
|
+
|
387
|
+
|
388
|
+
def test_run_with_extended_early_exit_run_disabled(
|
389
|
+
mocker: MockerFixture,
|
390
|
+
secret_reader: SecretReaderBase,
|
391
|
+
) -> None:
|
392
|
+
mocks = setup_mocks(
|
393
|
+
mocker,
|
394
|
+
secret_reader,
|
395
|
+
aws_accounts=[{"name": "a"}],
|
396
|
+
tf_namespaces=[],
|
397
|
+
)
|
398
|
+
|
399
|
+
integ.run(
|
400
|
+
True,
|
401
|
+
account_name="a",
|
402
|
+
enable_extended_early_exit=False,
|
403
|
+
)
|
404
|
+
|
405
|
+
mocks["extended_early_exit_run"].assert_not_called()
|
406
|
+
mocks["tf"].plan.assert_called_once_with(False)
|
407
|
+
|
408
|
+
|
409
|
+
def test_run_with_extended_early_exit_run_feature_disabled(
|
410
|
+
mocker: MockerFixture,
|
411
|
+
secret_reader: SecretReaderBase,
|
412
|
+
) -> None:
|
413
|
+
mocks = setup_mocks(
|
414
|
+
mocker,
|
415
|
+
secret_reader,
|
416
|
+
aws_accounts=[{"name": "a"}],
|
417
|
+
tf_namespaces=[],
|
418
|
+
feature_toggle_state=False,
|
419
|
+
)
|
420
|
+
|
421
|
+
integ.run(
|
422
|
+
True,
|
423
|
+
account_name="a",
|
424
|
+
enable_extended_early_exit=True,
|
425
|
+
)
|
426
|
+
|
427
|
+
mocks["extended_early_exit_run"].assert_not_called()
|
428
|
+
mocks["tf"].plan.assert_called_once_with(False)
|
429
|
+
mocks["get_feature_toggle_state"].assert_called_once_with(
|
430
|
+
"terraform-resources-extended-early-exit",
|
431
|
+
default=False,
|
432
|
+
)
|
433
|
+
|
434
|
+
|
435
|
+
def test_terraform_resources_runner_dry_run(
|
436
|
+
secret_reader: SecretReaderBase,
|
437
|
+
) -> None:
|
438
|
+
tf = create_autospec(integ.Terraform)
|
439
|
+
tf.plan.return_value = (False, None)
|
440
|
+
|
441
|
+
ts = create_autospec(integ.Terrascript)
|
442
|
+
terraform_configurations = {"a": "b"}
|
443
|
+
ts.terraform_configurations.return_value = terraform_configurations
|
444
|
+
|
445
|
+
defer = MagicMock()
|
446
|
+
|
447
|
+
runner_params = dict(
|
448
|
+
accounts=[{"name": "a"}],
|
449
|
+
account_names={"a"},
|
450
|
+
tf_namespaces=[],
|
451
|
+
tf=tf,
|
452
|
+
ts=ts,
|
453
|
+
secret_reader=secret_reader,
|
454
|
+
dry_run=True,
|
455
|
+
enable_deletion=False,
|
456
|
+
thread_pool_size=10,
|
457
|
+
internal=None,
|
458
|
+
use_jump_host=True,
|
459
|
+
light=False,
|
460
|
+
vault_output_path="",
|
461
|
+
defer=defer,
|
462
|
+
)
|
463
|
+
|
464
|
+
result = integ.runner(**runner_params)
|
465
|
+
|
466
|
+
assert result == integ.ExtendedEarlyExitRunnerResult(
|
467
|
+
payload=terraform_configurations,
|
468
|
+
applied_count=0,
|
469
|
+
)
|
470
|
+
|
471
|
+
|
472
|
+
def test_terraform_resources_runner_no_dry_run(
|
473
|
+
mocker: MockerFixture,
|
474
|
+
secret_reader: SecretReaderBase,
|
475
|
+
) -> None:
|
476
|
+
tf = create_autospec(integ.Terraform)
|
477
|
+
tf.plan.return_value = (False, None)
|
478
|
+
tf.apply_count = 1
|
479
|
+
tf.should_apply.return_value = True
|
480
|
+
tf.apply.return_value = False
|
481
|
+
|
482
|
+
ts = create_autospec(integ.Terrascript)
|
483
|
+
terraform_configurations = {"a": "b"}
|
484
|
+
ts.terraform_configurations.return_value = terraform_configurations
|
485
|
+
ts.resource_spec_inventory = {}
|
486
|
+
|
487
|
+
defer = MagicMock()
|
488
|
+
|
489
|
+
mocked_ob = mocker.patch("reconcile.terraform_resources.ob")
|
490
|
+
mocked_ob.realize_data.return_value = [{"action": "applied"}]
|
491
|
+
|
492
|
+
runner_params = dict(
|
493
|
+
accounts=[{"name": "a"}],
|
494
|
+
account_names={"a"},
|
495
|
+
tf_namespaces=[],
|
496
|
+
tf=tf,
|
497
|
+
ts=ts,
|
498
|
+
secret_reader=secret_reader,
|
499
|
+
dry_run=False,
|
500
|
+
enable_deletion=False,
|
501
|
+
thread_pool_size=10,
|
502
|
+
internal=None,
|
503
|
+
use_jump_host=True,
|
504
|
+
light=False,
|
505
|
+
vault_output_path="",
|
506
|
+
defer=defer,
|
507
|
+
)
|
508
|
+
|
509
|
+
result = integ.runner(**runner_params)
|
510
|
+
|
511
|
+
assert result == integ.ExtendedEarlyExitRunnerResult(
|
512
|
+
payload=terraform_configurations,
|
513
|
+
applied_count=2,
|
514
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
from datetime import UTC, datetime, timedelta
|
2
2
|
from enum import Enum
|
3
|
-
from typing import Any,
|
3
|
+
from typing import Any, Self
|
4
4
|
|
5
5
|
from deepdiff import DeepHash
|
6
6
|
from pydantic import BaseModel
|
@@ -16,19 +16,19 @@ class CacheKey(BaseModel):
|
|
16
16
|
integration: str
|
17
17
|
integration_version: str
|
18
18
|
dry_run: bool
|
19
|
-
|
19
|
+
cache_source: object
|
20
20
|
|
21
21
|
def __str__(self) -> str:
|
22
22
|
return "/".join([
|
23
23
|
self.integration,
|
24
24
|
self.integration_version,
|
25
25
|
"dry-run" if self.dry_run else "no-dry-run",
|
26
|
-
DeepHash(self.
|
26
|
+
DeepHash(self.cache_source)[self.cache_source],
|
27
27
|
])
|
28
28
|
|
29
29
|
|
30
30
|
class CacheValue(BaseModel):
|
31
|
-
|
31
|
+
payload: object
|
32
32
|
log_output: str
|
33
33
|
applied_count: int
|
34
34
|
|
@@ -46,7 +46,7 @@ class EarlyExitCache:
|
|
46
46
|
@classmethod
|
47
47
|
def build(
|
48
48
|
cls,
|
49
|
-
secret_reader:
|
49
|
+
secret_reader: SecretReaderBase | None = None,
|
50
50
|
) -> Self:
|
51
51
|
state = init_state(STATE_INTEGRATION, secret_reader)
|
52
52
|
return cls(state)
|
@@ -0,0 +1,177 @@
|
|
1
|
+
import logging
|
2
|
+
from collections.abc import Callable, Generator, Mapping
|
3
|
+
from contextlib import contextmanager
|
4
|
+
from io import StringIO
|
5
|
+
from logging import Logger
|
6
|
+
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from reconcile.utils.early_exit_cache import (
|
10
|
+
CacheKey,
|
11
|
+
CacheStatus,
|
12
|
+
CacheValue,
|
13
|
+
EarlyExitCache,
|
14
|
+
)
|
15
|
+
from reconcile.utils.metrics import (
|
16
|
+
CounterMetric,
|
17
|
+
GaugeMetric,
|
18
|
+
inc_counter,
|
19
|
+
normalize_integration_name,
|
20
|
+
set_gauge,
|
21
|
+
)
|
22
|
+
from reconcile.utils.secret_reader import SecretReaderBase
|
23
|
+
|
24
|
+
|
25
|
+
class ExtendedEarlyExitRunnerResult(BaseModel):
|
26
|
+
payload: object
|
27
|
+
applied_count: int
|
28
|
+
|
29
|
+
|
30
|
+
class ExtendedEarlyExitBaseMetric(BaseModel):
|
31
|
+
integration: str
|
32
|
+
integration_version: str
|
33
|
+
dry_run: bool
|
34
|
+
cache_status: str
|
35
|
+
|
36
|
+
|
37
|
+
class ExtendedEarlyExitCounter(ExtendedEarlyExitBaseMetric, CounterMetric):
|
38
|
+
@classmethod
|
39
|
+
def name(cls) -> str:
|
40
|
+
return "qontract_reconcile_extended_early_exit"
|
41
|
+
|
42
|
+
|
43
|
+
class ExtendedEarlyExitAppliedCountGauge(ExtendedEarlyExitBaseMetric, GaugeMetric):
|
44
|
+
@classmethod
|
45
|
+
def name(cls) -> str:
|
46
|
+
return "qontract_reconcile_extended_early_exit_applied_count"
|
47
|
+
|
48
|
+
|
49
|
+
def _publish_metrics(
|
50
|
+
cache_key: CacheKey,
|
51
|
+
cache_status: CacheStatus,
|
52
|
+
applied_count: int,
|
53
|
+
) -> None:
|
54
|
+
inc_counter(
|
55
|
+
ExtendedEarlyExitCounter(
|
56
|
+
integration=cache_key.integration,
|
57
|
+
integration_version=cache_key.integration_version,
|
58
|
+
dry_run=cache_key.dry_run,
|
59
|
+
cache_status=cache_status.value,
|
60
|
+
),
|
61
|
+
)
|
62
|
+
set_gauge(
|
63
|
+
ExtendedEarlyExitAppliedCountGauge(
|
64
|
+
integration=cache_key.integration,
|
65
|
+
integration_version=cache_key.integration_version,
|
66
|
+
dry_run=cache_key.dry_run,
|
67
|
+
cache_status=cache_status.value,
|
68
|
+
),
|
69
|
+
applied_count,
|
70
|
+
)
|
71
|
+
|
72
|
+
|
73
|
+
def _ttl_seconds(
|
74
|
+
applied_count: int,
|
75
|
+
ttl_seconds: int,
|
76
|
+
) -> int:
|
77
|
+
"""
|
78
|
+
Pick the ttl based on the applied count.
|
79
|
+
If the applied count is greater than 0, then we want to set ttl to 0 so that the next run will not hit the cache,
|
80
|
+
this will allow us to easy debug reconcile loops, as we will be able to see the logs of the next run,
|
81
|
+
and check cached value for more details.
|
82
|
+
|
83
|
+
:param applied_count: The number of resources that were applied
|
84
|
+
:param ttl_seconds: A ttl in seconds
|
85
|
+
:return: The ttl in seconds
|
86
|
+
"""
|
87
|
+
return 0 if applied_count > 0 else ttl_seconds
|
88
|
+
|
89
|
+
|
90
|
+
@contextmanager
|
91
|
+
def log_stream_handler(
|
92
|
+
logger: Logger,
|
93
|
+
) -> Generator[StringIO, None, None]:
|
94
|
+
"""
|
95
|
+
Add a stream handler to the logger, and return the stream generator, automatically remove the handler when done.
|
96
|
+
|
97
|
+
:param logger: A logger
|
98
|
+
:return: A stream generator
|
99
|
+
"""
|
100
|
+
log_stream = StringIO()
|
101
|
+
log_handler = logging.StreamHandler(log_stream)
|
102
|
+
logger.addHandler(log_handler)
|
103
|
+
try:
|
104
|
+
yield log_stream
|
105
|
+
finally:
|
106
|
+
logger.removeHandler(log_handler)
|
107
|
+
|
108
|
+
|
109
|
+
def extended_early_exit_run(
|
110
|
+
integration: str,
|
111
|
+
integration_version: str,
|
112
|
+
dry_run: bool,
|
113
|
+
cache_source: object,
|
114
|
+
ttl_seconds: int,
|
115
|
+
logger: Logger,
|
116
|
+
runner: Callable[..., ExtendedEarlyExitRunnerResult],
|
117
|
+
runner_params: Mapping | None = None,
|
118
|
+
secret_reader: SecretReaderBase | None = None,
|
119
|
+
log_cached_log_output: bool = False,
|
120
|
+
) -> None:
|
121
|
+
"""
|
122
|
+
Run the runner based on the cache status. Early exit when cache hit.
|
123
|
+
Runner log output will be extracted and stored in cache value,
|
124
|
+
and will be logged when hit if log_cached_log_output is True,
|
125
|
+
this is mainly used to show all log output from different integrations in one place (CI).
|
126
|
+
When runner returns no applies (applied_count is 0), the ttl will be set to ttl_seconds,
|
127
|
+
otherwise it will be set to 0.
|
128
|
+
|
129
|
+
:param integration: The integration name
|
130
|
+
:param integration_version: The integration version
|
131
|
+
:param dry_run: True if the run is in dry run mode, False otherwise
|
132
|
+
:param cache_source: The cache source, usually the static desired state
|
133
|
+
:param ttl_seconds: A ttl in seconds
|
134
|
+
:param logger: A Logger
|
135
|
+
:param runner: A runner can return ExtendedEarlyExitRunnerResult when called
|
136
|
+
:param runner_params: Runner params, will be spread into kwargs when calling runner
|
137
|
+
:param secret_reader: A secret reader
|
138
|
+
:param log_cached_log_output: Whether to log the cached log output when there is a cache hit
|
139
|
+
:return: None
|
140
|
+
"""
|
141
|
+
with EarlyExitCache.build(secret_reader) as cache:
|
142
|
+
key = CacheKey(
|
143
|
+
integration=normalize_integration_name(integration),
|
144
|
+
integration_version=integration_version,
|
145
|
+
dry_run=dry_run,
|
146
|
+
cache_source=cache_source,
|
147
|
+
)
|
148
|
+
cache_status = cache.head(key)
|
149
|
+
logger.debug("Early exit cache status for key=%s: %s", key, cache_status)
|
150
|
+
|
151
|
+
if cache_status == CacheStatus.HIT:
|
152
|
+
if log_cached_log_output:
|
153
|
+
logger.info(cache.get(key).log_output)
|
154
|
+
_publish_metrics(
|
155
|
+
cache_key=key,
|
156
|
+
cache_status=cache_status,
|
157
|
+
applied_count=0,
|
158
|
+
)
|
159
|
+
return
|
160
|
+
|
161
|
+
with log_stream_handler(logger) as log_stream:
|
162
|
+
result = runner(**(runner_params or {}))
|
163
|
+
log_output = log_stream.getvalue()
|
164
|
+
|
165
|
+
value = CacheValue(
|
166
|
+
payload=result.payload,
|
167
|
+
log_output=log_output,
|
168
|
+
applied_count=result.applied_count,
|
169
|
+
)
|
170
|
+
ttl = _ttl_seconds(result.applied_count, ttl_seconds)
|
171
|
+
logger.debug("Set early exit cache for key=%s with ttl=%d", key, ttl)
|
172
|
+
cache.set(key, value, ttl)
|
173
|
+
_publish_metrics(
|
174
|
+
cache_key=key,
|
175
|
+
cache_status=cache_status,
|
176
|
+
applied_count=result.applied_count,
|
177
|
+
)
|
@@ -79,11 +79,12 @@ def get_inventory_count_combinations(
|
|
79
79
|
|
80
80
|
def publish_metrics(inventory: ExternalResourceSpecInventory, integration: str) -> None:
|
81
81
|
count_combinations = get_inventory_count_combinations(inventory)
|
82
|
+
integration_name = metrics.normalize_integration_name(integration)
|
82
83
|
for combination, count in count_combinations.items():
|
83
84
|
provision_provider, provisioner_name, provider = combination
|
84
85
|
metrics.set_gauge(
|
85
86
|
ExternalResourceInventoryGauge(
|
86
|
-
integration=
|
87
|
+
integration=integration_name,
|
87
88
|
provision_provider=provision_provider,
|
88
89
|
provisioner_name=provisioner_name,
|
89
90
|
provider=provider,
|
reconcile/utils/metrics.py
CHANGED
@@ -563,3 +563,10 @@ class ErrorRateMetricSet:
|
|
563
563
|
if exc_value:
|
564
564
|
self.fail(exc_value)
|
565
565
|
inc_counter(self._error_counter, by=(1 if self._errors else 0))
|
566
|
+
|
567
|
+
|
568
|
+
def normalize_integration_name(integration: str) -> str:
|
569
|
+
"""
|
570
|
+
Normalize the integration name to be used in prometheus.
|
571
|
+
"""
|
572
|
+
return integration.replace("_", "-")
|
@@ -90,7 +90,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
90
90
|
self.thread_pool_size = thread_pool_size
|
91
91
|
self._aws_api = aws_api
|
92
92
|
self._log_lock = Lock()
|
93
|
-
self.
|
93
|
+
self.apply_count = 0
|
94
94
|
|
95
95
|
self.specs: list[TerraformSpec] = []
|
96
96
|
self.init_specs()
|
@@ -111,6 +111,12 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
111
111
|
for account, output in self.outputs.items()
|
112
112
|
}
|
113
113
|
|
114
|
+
def increment_apply_count(self):
|
115
|
+
self.apply_count += 1
|
116
|
+
|
117
|
+
def should_apply(self) -> bool:
|
118
|
+
return self.apply_count > 0
|
119
|
+
|
114
120
|
def get_new_users(self):
|
115
121
|
new_users = []
|
116
122
|
self.init_outputs() # get updated output
|
@@ -282,7 +288,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
282
288
|
after = output_change.get("after")
|
283
289
|
if before != after:
|
284
290
|
logging.info(["update", name, "output", output_name])
|
285
|
-
self.
|
291
|
+
self.increment_apply_count()
|
286
292
|
|
287
293
|
# A way to detect deleted outputs is by comparing
|
288
294
|
# the prior state with the output changes.
|
@@ -295,7 +301,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
295
301
|
deleted_outputs = [po for po in prior_outputs if po not in output_changes]
|
296
302
|
for output_name in deleted_outputs:
|
297
303
|
logging.info(["delete", name, "output", output_name])
|
298
|
-
self.
|
304
|
+
self.increment_apply_count()
|
299
305
|
|
300
306
|
resource_changes = output.get("resource_changes")
|
301
307
|
if resource_changes is None:
|
@@ -339,7 +345,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
339
345
|
resource_name,
|
340
346
|
self._resource_diff_changed_fields(action, resource_change),
|
341
347
|
])
|
342
|
-
self.
|
348
|
+
self.increment_apply_count()
|
343
349
|
if action == "create":
|
344
350
|
if resource_type == "aws_iam_user_login_profile":
|
345
351
|
created_users.append(AccountUser(name, resource_name))
|
@@ -3890,22 +3890,30 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
3890
3890
|
if os.path.isfile(print_to_file):
|
3891
3891
|
os.remove(print_to_file)
|
3892
3892
|
|
3893
|
-
for name, ts in self.
|
3893
|
+
for name, ts in self.terraform_configurations().items():
|
3894
3894
|
if print_to_file:
|
3895
3895
|
with open(print_to_file, "a", encoding="locale") as f:
|
3896
3896
|
f.write(f"##### {name} #####\n")
|
3897
|
-
f.write(
|
3897
|
+
f.write(ts)
|
3898
3898
|
f.write("\n")
|
3899
3899
|
if existing_dirs is None:
|
3900
3900
|
wd = tempfile.mkdtemp(prefix=TMP_DIR_PREFIX)
|
3901
3901
|
else:
|
3902
3902
|
wd = working_dirs[name]
|
3903
3903
|
with open(wd + "/config.tf.json", "w", encoding="locale") as f:
|
3904
|
-
f.write(
|
3904
|
+
f.write(ts)
|
3905
3905
|
working_dirs[name] = wd
|
3906
3906
|
|
3907
3907
|
return working_dirs
|
3908
3908
|
|
3909
|
+
def terraform_configurations(self) -> dict[str, str]:
|
3910
|
+
"""
|
3911
|
+
Return the Terraform configurations (in JSON format) for each AWS account.
|
3912
|
+
|
3913
|
+
:return: key is AWS account name and value is terraform configuration
|
3914
|
+
"""
|
3915
|
+
return {name: str(ts) for name, ts in self.tss.items()}
|
3916
|
+
|
3909
3917
|
def init_values(self, spec: ExternalResourceSpec, init_tags: bool = True) -> dict:
|
3910
3918
|
"""
|
3911
3919
|
Initialize the values of the terraform resource and merge the defaults and
|
tools/qontract_cli.py
CHANGED
@@ -2446,8 +2446,8 @@ def early_exit_cache(ctx):
|
|
2446
2446
|
)
|
2447
2447
|
@click.option(
|
2448
2448
|
"-c",
|
2449
|
-
"--cache-
|
2450
|
-
help="Cache
|
2449
|
+
"--cache-source",
|
2450
|
+
help="Cache source. It should be a JSON string.",
|
2451
2451
|
required=True,
|
2452
2452
|
)
|
2453
2453
|
@click.pass_context
|
@@ -2456,14 +2456,14 @@ def early_exit_cache_head(
|
|
2456
2456
|
integration,
|
2457
2457
|
integration_version,
|
2458
2458
|
dry_run,
|
2459
|
-
|
2459
|
+
cache_source,
|
2460
2460
|
):
|
2461
2461
|
with EarlyExitCache.build() as cache:
|
2462
2462
|
cache_key = CacheKey(
|
2463
2463
|
integration=integration,
|
2464
2464
|
integration_version=integration_version,
|
2465
2465
|
dry_run=dry_run,
|
2466
|
-
|
2466
|
+
cache_source=json.loads(cache_source),
|
2467
2467
|
)
|
2468
2468
|
status = cache.head(cache_key)
|
2469
2469
|
print(status)
|
@@ -2489,8 +2489,8 @@ def early_exit_cache_head(
|
|
2489
2489
|
)
|
2490
2490
|
@click.option(
|
2491
2491
|
"-c",
|
2492
|
-
"--cache-
|
2493
|
-
help="Cache
|
2492
|
+
"--cache-source",
|
2493
|
+
help="Cache source. It should be a JSON string.",
|
2494
2494
|
required=True,
|
2495
2495
|
)
|
2496
2496
|
@click.pass_context
|
@@ -2499,14 +2499,14 @@ def early_exit_cache_get(
|
|
2499
2499
|
integration,
|
2500
2500
|
integration_version,
|
2501
2501
|
dry_run,
|
2502
|
-
|
2502
|
+
cache_source,
|
2503
2503
|
):
|
2504
2504
|
with EarlyExitCache.build() as cache:
|
2505
2505
|
cache_key = CacheKey(
|
2506
2506
|
integration=integration,
|
2507
2507
|
integration_version=integration_version,
|
2508
2508
|
dry_run=dry_run,
|
2509
|
-
|
2509
|
+
cache_source=json.loads(cache_source),
|
2510
2510
|
)
|
2511
2511
|
value = cache.get(cache_key)
|
2512
2512
|
print(value)
|
@@ -2532,14 +2532,14 @@ def early_exit_cache_get(
|
|
2532
2532
|
)
|
2533
2533
|
@click.option(
|
2534
2534
|
"-c",
|
2535
|
-
"--cache-
|
2536
|
-
help="Cache
|
2535
|
+
"--cache-source",
|
2536
|
+
help="Cache source. It should be a JSON string.",
|
2537
2537
|
required=True,
|
2538
2538
|
)
|
2539
2539
|
@click.option(
|
2540
|
-
"-
|
2541
|
-
"--
|
2542
|
-
help="
|
2540
|
+
"-p",
|
2541
|
+
"--payload",
|
2542
|
+
help="Payload in Cache value. It should be a JSON string.",
|
2543
2543
|
required=True,
|
2544
2544
|
)
|
2545
2545
|
@click.option(
|
@@ -2568,8 +2568,8 @@ def early_exit_cache_set(
|
|
2568
2568
|
integration,
|
2569
2569
|
integration_version,
|
2570
2570
|
dry_run,
|
2571
|
-
|
2572
|
-
|
2571
|
+
cache_source,
|
2572
|
+
payload,
|
2573
2573
|
log_output,
|
2574
2574
|
applied_count,
|
2575
2575
|
ttl,
|
@@ -2579,10 +2579,10 @@ def early_exit_cache_set(
|
|
2579
2579
|
integration=integration,
|
2580
2580
|
integration_version=integration_version,
|
2581
2581
|
dry_run=dry_run,
|
2582
|
-
|
2582
|
+
cache_source=json.loads(cache_source),
|
2583
2583
|
)
|
2584
2584
|
cache_value = CacheValue(
|
2585
|
-
|
2585
|
+
payload=json.loads(payload),
|
2586
2586
|
log_output=log_output,
|
2587
2587
|
applied_count=applied_count,
|
2588
2588
|
)
|
tools/test/test_qontract_cli.py
CHANGED
@@ -84,7 +84,7 @@ def test_early_exit_cache_set(env_vars, mock_queries, mock_early_exit_cache):
|
|
84
84
|
|
85
85
|
result = runner.invoke(
|
86
86
|
qontract_cli.early_exit_cache,
|
87
|
-
"set -i a -v b --no-dry-run -c {} -
|
87
|
+
"set -i a -v b --no-dry-run -c {} -p {} -l log -t 30",
|
88
88
|
)
|
89
89
|
assert result.exit_code == 0
|
90
90
|
mock_early_exit_cache.build.return_value.__enter__.return_value.set.assert_called()
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc538.dist-info}/top_level.txt
RENAMED
File without changes
|