qontract-reconcile 0.10.1rc705__py3-none-any.whl → 0.10.1rc707__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.1rc705
3
+ Version: 0.10.1rc707
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
@@ -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=deAj6fNIYnrEKgOKv2UYZKAvuvEmYVUWj4nDSG6y99U,96070
12
+ reconcile/cli.py,sha256=Xci_L0O3nPUPK3HA1NhIhbrzcPLZkXT65fCaJA66Tvg,96354
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
@@ -18,6 +18,7 @@ reconcile/dashdotdb_dora.py,sha256=n9EJXhxCoMYuldj4Fa5s0TqfiiolSrqDEOCaLBV3uag,1
18
18
  reconcile/dashdotdb_dvo.py,sha256=YXqpI6fBQAql-ybGI0grj9gWMzmKiAvPE__pNju6obk,8996
19
19
  reconcile/dashdotdb_slo.py,sha256=bf1WSh5JP9obHVQsMy0OO71_VTYZgwAopElFZM6DmRo,6714
20
20
  reconcile/database_access_manager.py,sha256=42dBJyihdwx4WjEBjwi3lUiDzQ1t_2ZFViJri2c4_aE,25716
21
+ reconcile/deadmanssnitch.py,sha256=lePZWxya1Xz_EqKxyBXoeTeFSWuJEn0-mU91SEHs93g,7707
21
22
  reconcile/dynatrace_token_provider.py,sha256=P5jvMavremWp64LVknz1kCZI4aagwLrDDfXkmJ9diwY,17212
22
23
  reconcile/email_sender.py,sha256=-5L-Ag_jaEYSzYRoMr52KQBRXz1E8yx9GqLbg2X4XFU,3533
23
24
  reconcile/gabi_authorized_users.py,sha256=9kpSJGyMe_qYVHIgTFHhYf8E3lKSLO0Ia1WwK9ADNIE,4502
@@ -136,7 +137,7 @@ reconcile/aus/version_gates/ingress_gate_handler.py,sha256=ZCtyggBzzcb0prtdbMpJs
136
137
  reconcile/aus/version_gates/ocp_gate_handler.py,sha256=RW1ppDaCZXVegV9AzzqYXxDUu_Z_7d43Z5h2Pk_piKc,716
137
138
  reconcile/aus/version_gates/sts_version_gate_handler.py,sha256=PhJ7yBh2q-rv9CJcfFhc0H11nyDyG7NAryNS3F74xdY,3697
138
139
  reconcile/aws_account_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
139
- reconcile/aws_account_manager/integration.py,sha256=eYWP5zrLwSOYcAN8ym0RxU1Gri_Fwovho_X1AxUQMXc,15049
140
+ reconcile/aws_account_manager/integration.py,sha256=FRZ1_jaK4maY4ClJmVGB7dCkDxZCaQP4yMDlYF-Lc3E,15042
140
141
  reconcile/aws_account_manager/merge_request_manager.py,sha256=zZct3NxWMBQupl4QfD7ULxnt4ipt_2FBoH_NusboIuw,3781
141
142
  reconcile/aws_account_manager/metrics.py,sha256=YB10ea4kIGwJfs5N14RF-RoXPb-QQWaDBz1jLZ3YWE0,917
142
143
  reconcile/aws_account_manager/reconciler.py,sha256=AqAA3TIEfuYzIogHSBgwYTebxbTy1D6JhcxdLiOfCsc,13588
@@ -215,12 +216,14 @@ reconcile/gql_definitions/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
215
216
  reconcile/gql_definitions/common/alerting_services_settings.py,sha256=mT7cobC9mR_bhFSYeQX1apVA5zMqSbu5fYcdd4iZ9mg,1802
216
217
  reconcile/gql_definitions/common/app_code_component_repos.py,sha256=eGlv8SRiLl-QUYNDl_gAvtQGMB2Hr31EqUXYQO_wOcg,1835
217
218
  reconcile/gql_definitions/common/app_interface_custom_messages.py,sha256=5a-rmQqR0qmews01K1WiTw1ZHUIUmQoznF9v8_Uw9QQ,2000
219
+ reconcile/gql_definitions/common/app_interface_dms_settings.py,sha256=h-N7-XGpmH7O9d3UBPhJJl48tpZvqihLloamZKdlgVg,2568
218
220
  reconcile/gql_definitions/common/app_interface_repo_settings.py,sha256=rud0rz9NIFF-h1fFdk3MnwGmx73rhwrn1taN_HefvyU,1754
219
221
  reconcile/gql_definitions/common/app_interface_state_settings.py,sha256=VXIK0Hmyv6GTShI86IGkjxyHGwufqUBAh617XKUAKaI,2507
220
222
  reconcile/gql_definitions/common/app_interface_vault_settings.py,sha256=w8quvdG0cSq71ZyJokPPp7MyMpoDb6-HLQ3o9JHVGRQ,1771
221
223
  reconcile/gql_definitions/common/aws_vpcs.py,sha256=Dss9dQ3xagnz3Ltg1e9mtG2PAmQGBbUzKCmmzvuN28s,1892
222
224
  reconcile/gql_definitions/common/clusters.py,sha256=eJIbMQltj-Sc7h_QVIeFkbRy1b_QJIkHATfbagxM-yU,21527
223
225
  reconcile/gql_definitions/common/clusters_minimal.py,sha256=yZpjS9qWyusCEiWtD8wzf0tak298uQyxiN4lC6KNo4s,4475
226
+ reconcile/gql_definitions/common/clusters_with_dms.py,sha256=GJ53P8tgMLh1NfVkaV9_AmaqF9pNUqJZcDkcKzKzUy0,2242
224
227
  reconcile/gql_definitions/common/clusters_with_peering.py,sha256=6mv1m8lc9UDzMWXatkTbYHwBaXoHIIC62qXULsfQN5Y,11822
225
228
  reconcile/gql_definitions/common/github_orgs.py,sha256=rZ0pDAA2_9hF9N-ykRZIxPtEmczTSjuA_k3nkp0k1W0,2039
226
229
  reconcile/gql_definitions/common/jira_settings.py,sha256=Fmjxhlhr69kc4jkG_0k17fuYlQVucbNex0jXYu83wbY,1990
@@ -436,6 +439,7 @@ reconcile/test/test_cli.py,sha256=qx_iBwh4Z-YkK3sbjK1wEziPTgn060EN-baf9DNvR3k,10
436
439
  reconcile/test/test_closedbox_endpoint_monitoring.py,sha256=isMHYwRWMFARU2nbJgbl69kD6H0eA86noCM4MPVI1fo,7151
437
440
  reconcile/test/test_dashdotdb_dora.py,sha256=MfHGAsX2eSQSvBVt9_1Sah3aQKNJBXA9Iu86X0NWD6c,7705
438
441
  reconcile/test/test_database_access_manager.py,sha256=-9fYo8wMNhbJUTK_bd7g_fS5zYsAlqQ0rBDDYBMZvZQ,19595
442
+ reconcile/test/test_deadmanssnitch.py,sha256=tSeFRG9JUOR-Y7W4S5adv9ZqfgAhbQOgnqW-EhCXggU,9445
439
443
  reconcile/test/test_gabi_authorized_users.py,sha256=6XnV5Q9inxP81ktGMVKyWucjBTUj8Imy2L0HG3YHyUE,2496
440
444
  reconcile/test/test_gcr_mirror.py,sha256=A0y8auKZzr62-mGoxSQ__JnN0-ijZUltzjwR5miBgso,490
441
445
  reconcile/test/test_github_org.py,sha256=j3KeB4OnSln1gm2hidce49xdMru-j75NS3cM-AEgzZc,4511
@@ -534,12 +538,14 @@ reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas
534
538
  reconcile/typed_queries/__init__.py,sha256=rRk4CyslLsBr4vAh1pIPgt6s3P4R1M9NSEPLnyQgBpk,61
535
539
  reconcile/typed_queries/alerting_services_settings.py,sha256=sX6s8GY-BB0UHogMC1ICeREVab-IYrNm1c-hqMGEdYQ,864
536
540
  reconcile/typed_queries/app_interface_custom_messages.py,sha256=5HWr68_kb4bEL8pDCIH0ez6GOrdwdbGF6w88xV0_Ccs,718
541
+ reconcile/typed_queries/app_interface_deadmanssnitch_settings.py,sha256=y30_SmS0JkxUN1qbpNqYnl8gEUBmom8UIEH4i9_Pl2E,683
537
542
  reconcile/typed_queries/app_interface_repo_url.py,sha256=ePr_nvjDeyZPR4sHsDQFubV2gslj9lPUSzIWWhm0syo,609
538
543
  reconcile/typed_queries/app_interface_state_settings.py,sha256=ytNAf3feg8JwmdCC4vO1WtVb-Ok5_ZtNAL3mXe2d1Pw,479
539
544
  reconcile/typed_queries/app_interface_vault_settings.py,sha256=uFS7qLjjGnrK8gm2fkUT8RFd7s_5kxTWWpxZRhr1Ndo,746
540
545
  reconcile/typed_queries/aws_vpcs.py,sha256=bM7dWpVHV6J1WYjgHOe2rfAl4vKpUoC8khPQSdtkxVQ,371
541
546
  reconcile/typed_queries/clusters.py,sha256=tptLP2Ov1lNqtDGuPKBOVvpDtREm5CQFDDVyp0xKkRQ,506
542
547
  reconcile/typed_queries/clusters_minimal.py,sha256=flvs4oXVR4b971h-zMTK3C2GL3bjMnvZgrAeI3XgBQQ,517
548
+ reconcile/typed_queries/clusters_with_dms.py,sha256=hyNPaHBgUIhnAeuOSY5SG46U1zDp9KytUXEobkZdRlQ,551
543
549
  reconcile/typed_queries/clusters_with_peering.py,sha256=lIai7SJJD0bqIJbe7virgrbYRqjLouSL2OpJD0itpAY,330
544
550
  reconcile/typed_queries/get_state_aws_account.py,sha256=CSJjVPWsUZ2rkGIt8ehoQt7hokFqrUDgG9HFlg2lVD8,492
545
551
  reconcile/typed_queries/github_orgs.py,sha256=UZhoPl8qvA_tcO7CZlN8GuMKckt3ywd47Suu61rgHsc,258
@@ -572,6 +578,7 @@ reconcile/utils/binary.py,sha256=EsOGg82Y2QJh91SGJE0tYpBKqU0iaaagQVoYONBtQn8,235
572
578
  reconcile/utils/config.py,sha256=aId5zrPjM_84u_T4yTRE_Psu3zo5-5_JCR6_7Wgv5UQ,990
573
579
  reconcile/utils/constants.py,sha256=pOUd97bqZdsAu5RWJ8NUs9cwCY7K9y0eW9VVeJ4fZIU,138
574
580
  reconcile/utils/data_structures.py,sha256=VyKfnlNJTiRvZKNpfgIrjESQ2YgmEpWuPQXT14WA1vI,311
581
+ reconcile/utils/deadmanssnitch_api.py,sha256=hkfbfbRAhzLpI39o6Du7FZKVtf4UVJ1OljOQNUkmODM,2478
575
582
  reconcile/utils/defer.py,sha256=SniUsbgOEs9Pa8JkecLu0F94O63yQPByKXaElDYe0FI,377
576
583
  reconcile/utils/differ.py,sha256=kJmUp9ZffFPSUEviaAw3s9c92ErwRJeHaRexGPai7wA,7643
577
584
  reconcile/utils/disabled_integrations.py,sha256=avdDsFyl_LdTsrPVzlcIhWzT_V4C4MXw1ZC__aOtluE,1126
@@ -754,8 +761,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
754
761
  tools/test/test_qontract_cli.py,sha256=UEwAW7PA_GIrbqzaLxpkCxbuVjEFLNvnVG-6VyoCGIc,4147
755
762
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
756
763
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
757
- qontract_reconcile-0.10.1rc705.dist-info/METADATA,sha256=YtGSBbEgiTX-KjpVXGWDJUurUpGAlTHRnByM79rRMd4,2382
758
- qontract_reconcile-0.10.1rc705.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
759
- qontract_reconcile-0.10.1rc705.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
760
- qontract_reconcile-0.10.1rc705.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
761
- qontract_reconcile-0.10.1rc705.dist-info/RECORD,,
764
+ qontract_reconcile-0.10.1rc707.dist-info/METADATA,sha256=2sMuicUof6ZemdDsl2x77zDnjVZkgPZowU7C4iUrFPk,2382
765
+ qontract_reconcile-0.10.1rc707.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
766
+ qontract_reconcile-0.10.1rc707.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
767
+ qontract_reconcile-0.10.1rc707.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
768
+ qontract_reconcile-0.10.1rc707.dist-info/RECORD,,
@@ -320,7 +320,7 @@ class AwsAccountMgmtIntegration(
320
320
  flavor=self.params.flavor,
321
321
  payer_account=payer_account.name,
322
322
  ),
323
- value=sum([len(payer_account.organization_accounts or [])]),
323
+ value=len(payer_account.organization_accounts or []),
324
324
  )
325
325
  metrics_container.set_gauge(
326
326
  NonOrgAccountCounter(flavor=self.params.flavor),
reconcile/cli.py CHANGED
@@ -3431,6 +3431,17 @@ def acs_policies(ctx):
3431
3431
  )
3432
3432
 
3433
3433
 
3434
+ @integration.command(short_help="Automate Deadmanssnitch Creation/Deletion")
3435
+ @click.pass_context
3436
+ def deadmanssnitch(ctx):
3437
+ from reconcile import deadmanssnitch
3438
+
3439
+ run_class_integration(
3440
+ integration=deadmanssnitch.DeadMansSnitchIntegration(),
3441
+ ctx=ctx.obj,
3442
+ )
3443
+
3444
+
3434
3445
  def get_integration_cli_meta() -> dict[str, IntegrationMeta]:
3435
3446
  """
3436
3447
  returns all integrations known to cli.py via click introspection
@@ -0,0 +1,208 @@
1
+ import logging
2
+ from typing import (
3
+ Optional,
4
+ cast,
5
+ )
6
+
7
+ from reconcile.gql_definitions.common.clusters_with_dms import ClusterV1
8
+ from reconcile.typed_queries.app_interface_deadmanssnitch_settings import (
9
+ get_deadmanssnitch_settings,
10
+ )
11
+ from reconcile.typed_queries.clusters_with_dms import get_clusters_with_dms
12
+ from reconcile.utils.deadmanssnitch_api import (
13
+ DeadMansSnitchApi,
14
+ Snitch,
15
+ )
16
+ from reconcile.utils.differ import diff_mappings
17
+ from reconcile.utils.runtime.integration import (
18
+ NoParams,
19
+ QontractReconcileIntegration,
20
+ )
21
+ from reconcile.utils.secret_reader import (
22
+ SecretNotFound,
23
+ )
24
+ from reconcile.utils.semver_helper import make_semver
25
+ from reconcile.utils.vault import (
26
+ SecretFieldNotFound,
27
+ VaultClient,
28
+ _VaultClient,
29
+ )
30
+
31
+ QONTRACT_INTEGRATION = "deadmanssnitch"
32
+ SECRET_NOT_FOUND = "SECRET_NOT_FOUND"
33
+
34
+
35
+ class DeadMansSnitchIntegration(QontractReconcileIntegration[NoParams]):
36
+ """Integration to automate deadmanssnitch snitch api during cluster dressup."""
37
+
38
+ @property
39
+ def name(self) -> str:
40
+ return QONTRACT_INTEGRATION
41
+
42
+ def __init__(self) -> None:
43
+ super().__init__(NoParams())
44
+ self.qontract_integration_version = make_semver(0, 1, 0)
45
+ self.settings = get_deadmanssnitch_settings()
46
+ self.vault_client = cast(_VaultClient, VaultClient())
47
+
48
+ def write_snitch_to_vault(
49
+ self, cluster_name: str, snitch_url: Optional[str]
50
+ ) -> None:
51
+ if snitch_url:
52
+ self.vault_client.write(
53
+ {
54
+ "path": self.settings.snitches_path,
55
+ "data": {f"deadmanssnitch-{cluster_name}-url": snitch_url},
56
+ },
57
+ decode_base64=False,
58
+ )
59
+
60
+ def add_vault_data(
61
+ self, cluster_name: str, snitch: Snitch, snitch_secret_path: str
62
+ ) -> Snitch:
63
+ try:
64
+ full_secret_path = {
65
+ "path": snitch_secret_path,
66
+ "field": f"deadmanssnitch-{cluster_name}-url",
67
+ }
68
+ snitch.vault_data = self.secret_reader.read(full_secret_path).strip()
69
+ except (SecretNotFound, SecretFieldNotFound):
70
+ snitch.vault_data = SECRET_NOT_FOUND
71
+ return snitch
72
+
73
+ def create_snitch(
74
+ self, cluster_name: str, snitch: Snitch, deadmanssnitch_api: DeadMansSnitchApi
75
+ ) -> None:
76
+ payload = {
77
+ "name": snitch.name,
78
+ "alert_type": snitch.alert_type,
79
+ "interval": snitch.interval,
80
+ "tags": snitch.tags,
81
+ "alert_email": snitch.alert_email,
82
+ "notes": snitch.notes,
83
+ }
84
+ snitch_data = deadmanssnitch_api.create_snitch(payload=payload)
85
+ self.write_snitch_to_vault(
86
+ cluster_name=cluster_name, snitch_url=snitch_data.check_in_url
87
+ )
88
+
89
+ def reconcile(
90
+ self,
91
+ dry_run: bool,
92
+ current_state: dict[str, Snitch],
93
+ desired_state: dict[str, Snitch],
94
+ deadmanssnitch_api: DeadMansSnitchApi,
95
+ ) -> None:
96
+ diffs = diff_mappings(current=current_state, desired=desired_state)
97
+ errors = []
98
+ for cluster_name, snitch in diffs.add.items():
99
+ logging.info("[cluster_name:%s] [Action:create_snitch]", cluster_name)
100
+ try:
101
+ if not dry_run:
102
+ self.create_snitch(cluster_name, snitch, deadmanssnitch_api)
103
+ except Exception as e:
104
+ errors.append(e)
105
+ for cluster_name, snitch in diffs.delete.items():
106
+ logging.info("[cluster_name:%s] [Action:delete_snitch]", cluster_name)
107
+ try:
108
+ if not dry_run:
109
+ deadmanssnitch_api.delete_snitch(snitch.token)
110
+ except Exception as e:
111
+ errors.append(e)
112
+ for cluster_name, diff_pair in diffs.identical.items():
113
+ try:
114
+ if diff_pair.desired.needs_vault_update():
115
+ self.write_snitch_to_vault(
116
+ cluster_name=cluster_name,
117
+ snitch_url=diff_pair.desired.check_in_url,
118
+ )
119
+ except Exception as e:
120
+ errors.append(e)
121
+ if errors:
122
+ raise ExceptionGroup("Errors occurred while reconcile", errors)
123
+
124
+ def get_current_state(
125
+ self,
126
+ deadmanssnitch_api: DeadMansSnitchApi,
127
+ clusters: list[ClusterV1],
128
+ snitch_secret_path: str,
129
+ cluster_to_prometheus_mapping: dict[str, str],
130
+ ) -> dict[str, Snitch]:
131
+ # current state includes for deadmanssnithch response and associated secret in vault
132
+ snitches = deadmanssnitch_api.get_snitches(tags=self.settings.tags)
133
+ # create snitch_map only for the desired clusters
134
+ snitches_with_cluster_mapping = {
135
+ cluster.name: snitch
136
+ for snitch in snitches
137
+ for cluster in clusters
138
+ if (cluster_to_prometheus_mapping.get(cluster.name) == snitch.name)
139
+ }
140
+ current_state = {
141
+ cluster.name: self.add_vault_data(cluster.name, snitch, snitch_secret_path)
142
+ for cluster in clusters
143
+ if (snitch := snitches_with_cluster_mapping.get(cluster.name))
144
+ }
145
+ return current_state
146
+
147
+ def get_desired_state(
148
+ self,
149
+ clusters: list[ClusterV1],
150
+ current_state: dict[str, Snitch],
151
+ cluster_to_prometheus_mapping: dict[str, str],
152
+ ) -> dict[str, Snitch]:
153
+ desired_state = {
154
+ cluster.name: self.map_desired_snitch_value(
155
+ cluster.name, current_state, cluster_to_prometheus_mapping
156
+ )
157
+ for cluster in clusters
158
+ if cluster.enable_dead_mans_snitch
159
+ }
160
+ return desired_state
161
+
162
+ # get snitch in case snitch available for desired state cluster
163
+ # return new snitch object in case of new cluster
164
+ def map_desired_snitch_value(
165
+ self,
166
+ cluster_name: str,
167
+ current_state: dict[str, Snitch],
168
+ cluster_to_prometheus_mapping: dict[str, str],
169
+ ) -> Snitch:
170
+ if (snitch := current_state.get(cluster_name)) is not None:
171
+ return snitch
172
+ return Snitch(
173
+ name=cluster_to_prometheus_mapping.get(cluster_name),
174
+ check_in_url="",
175
+ status="",
176
+ href="",
177
+ token="",
178
+ alert_email=self.settings.alert_mail_addresses,
179
+ alert_type=self.settings.alert_type,
180
+ interval=self.settings.interval,
181
+ tags=self.settings.tags,
182
+ notes=self.settings.notes_link,
183
+ )
184
+
185
+ def run(self, dry_run: bool) -> None:
186
+ # Initialize deadmanssnitch_api
187
+ token = self.secret_reader.read({
188
+ "path": self.settings.token_creds.path,
189
+ "field": self.settings.token_creds.field,
190
+ })
191
+ with DeadMansSnitchApi(token=token) as deadmanssnitch_api:
192
+ # desired state - get the clusters having enableDeadMansSnitch field
193
+ clusters = get_clusters_with_dms()
194
+ # create a mapping between prometheus url without the https:// and cluster name
195
+ cluster_to_prometheus_mapping = {
196
+ cluster.name: cluster.prometheus_url.replace("https://", "")
197
+ for cluster in clusters
198
+ }
199
+ current_state = self.get_current_state(
200
+ deadmanssnitch_api,
201
+ clusters,
202
+ self.settings.snitches_path,
203
+ cluster_to_prometheus_mapping,
204
+ )
205
+ desired_state = self.get_desired_state(
206
+ clusters, current_state, cluster_to_prometheus_mapping
207
+ )
208
+ self.reconcile(dry_run, current_state, desired_state, deadmanssnitch_api)
@@ -0,0 +1,86 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ DEFINITION = """
22
+ query DeadMansSnitchSettings {
23
+ settings: app_interface_settings_v1 {
24
+ deadMansSnitchSettings {
25
+ alertMailAddresses
26
+ notesLink
27
+ snitchesPath
28
+ tokenCreds {
29
+ path
30
+ field
31
+ }
32
+ tags
33
+ alertType
34
+ interval
35
+ }
36
+ }
37
+ }
38
+ """
39
+
40
+
41
+ class ConfiguredBaseModel(BaseModel):
42
+ class Config:
43
+ smart_union=True
44
+ extra=Extra.forbid
45
+
46
+
47
+ class VaultSecretV1(ConfiguredBaseModel):
48
+ path: str = Field(..., alias="path")
49
+ field: str = Field(..., alias="field")
50
+
51
+
52
+ class DeadMansSnitchSettingsV1(ConfiguredBaseModel):
53
+ alert_mail_addresses: list[str] = Field(..., alias="alertMailAddresses")
54
+ notes_link: str = Field(..., alias="notesLink")
55
+ snitches_path: str = Field(..., alias="snitchesPath")
56
+ token_creds: VaultSecretV1 = Field(..., alias="tokenCreds")
57
+ tags: list[str] = Field(..., alias="tags")
58
+ alert_type: str = Field(..., alias="alertType")
59
+ interval: str = Field(..., alias="interval")
60
+
61
+
62
+ class AppInterfaceSettingsV1(ConfiguredBaseModel):
63
+ dead_mans_snitch_settings: Optional[DeadMansSnitchSettingsV1] = Field(..., alias="deadMansSnitchSettings")
64
+
65
+
66
+ class DeadMansSnitchSettingsQueryData(ConfiguredBaseModel):
67
+ settings: Optional[list[AppInterfaceSettingsV1]] = Field(..., alias="settings")
68
+
69
+
70
+ def query(query_func: Callable, **kwargs: Any) -> DeadMansSnitchSettingsQueryData:
71
+ """
72
+ This is a convenience function which queries and parses the data into
73
+ concrete types. It should be compatible with most GQL clients.
74
+ You do not have to use it to consume the generated data classes.
75
+ Alternatively, you can also mime and alternate the behavior
76
+ of this function in the caller.
77
+
78
+ Parameters:
79
+ query_func (Callable): Function which queries your GQL Server
80
+ kwargs: optional arguments that will be passed to the query function
81
+
82
+ Returns:
83
+ DeadMansSnitchSettingsQueryData: queried data parsed into generated classes
84
+ """
85
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
86
+ return DeadMansSnitchSettingsQueryData(**raw_data)
@@ -0,0 +1,72 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ DEFINITION = """
22
+ query ClustersWithMonitoring($filter: JSON){
23
+ clusters: clusters_v1(filter: $filter) {
24
+ name
25
+ serverUrl
26
+ consoleUrl
27
+ alertmanagerUrl
28
+ prometheusUrl
29
+ managedClusterRoles
30
+ enableDeadMansSnitch
31
+ }
32
+ }
33
+ """
34
+
35
+
36
+ class ConfiguredBaseModel(BaseModel):
37
+ class Config:
38
+ smart_union=True
39
+ extra=Extra.forbid
40
+
41
+
42
+ class ClusterV1(ConfiguredBaseModel):
43
+ name: str = Field(..., alias="name")
44
+ server_url: str = Field(..., alias="serverUrl")
45
+ console_url: str = Field(..., alias="consoleUrl")
46
+ alertmanager_url: str = Field(..., alias="alertmanagerUrl")
47
+ prometheus_url: str = Field(..., alias="prometheusUrl")
48
+ managed_cluster_roles: Optional[bool] = Field(..., alias="managedClusterRoles")
49
+ enable_dead_mans_snitch: Optional[bool] = Field(..., alias="enableDeadMansSnitch")
50
+
51
+
52
+ class ClustersWithMonitoringQueryData(ConfiguredBaseModel):
53
+ clusters: Optional[list[ClusterV1]] = Field(..., alias="clusters")
54
+
55
+
56
+ def query(query_func: Callable, **kwargs: Any) -> ClustersWithMonitoringQueryData:
57
+ """
58
+ This is a convenience function which queries and parses the data into
59
+ concrete types. It should be compatible with most GQL clients.
60
+ You do not have to use it to consume the generated data classes.
61
+ Alternatively, you can also mime and alternate the behavior
62
+ of this function in the caller.
63
+
64
+ Parameters:
65
+ query_func (Callable): Function which queries your GQL Server
66
+ kwargs: optional arguments that will be passed to the query function
67
+
68
+ Returns:
69
+ ClustersWithMonitoringQueryData: queried data parsed into generated classes
70
+ """
71
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
72
+ return ClustersWithMonitoringQueryData(**raw_data)
@@ -0,0 +1,279 @@
1
+ from unittest.mock import MagicMock, create_autospec
2
+
3
+ import pytest
4
+ from pytest_mock import MockerFixture
5
+
6
+ from reconcile.deadmanssnitch import (
7
+ DeadMansSnitchIntegration,
8
+ )
9
+ from reconcile.gql_definitions.common.app_interface_dms_settings import (
10
+ DeadMansSnitchSettingsV1,
11
+ VaultSecretV1,
12
+ )
13
+ from reconcile.gql_definitions.common.clusters_with_dms import (
14
+ ClusterV1,
15
+ )
16
+ from reconcile.utils.deadmanssnitch_api import (
17
+ DeadMansSnitchApi,
18
+ Snitch,
19
+ )
20
+
21
+
22
+ @pytest.fixture
23
+ def deadmanssnitch_api() -> MockerFixture:
24
+ return create_autospec(DeadMansSnitchApi)
25
+
26
+
27
+ @pytest.fixture
28
+ def deadmanssnitch_settings() -> DeadMansSnitchSettingsV1:
29
+ settings = DeadMansSnitchSettingsV1(
30
+ alertMailAddresses=["test_email"],
31
+ notesLink="test_link",
32
+ snitchesPath="test_snitches_path",
33
+ tokenCreds=VaultSecretV1(path="test_path", field="test_field"),
34
+ tags=["test-tags"],
35
+ alertType="Heartbeat",
36
+ interval="15_minute",
37
+ )
38
+ return settings
39
+
40
+
41
+ @pytest.fixture
42
+ def vault_mock(mocker: MockerFixture) -> MockerFixture:
43
+ return mocker.patch("reconcile.utils.vault._VaultClient")
44
+
45
+
46
+ @pytest.fixture
47
+ def secret_reader(mocker: MockerFixture) -> MockerFixture:
48
+ mock_secretreader = mocker.patch(
49
+ "reconcile.utils.secret_reader.SecretReader", autospec=True
50
+ )
51
+ mock_secretreader.read.return_value = "secret"
52
+ mock_secretreader.read_secret.return_value = "secret"
53
+ return mock_secretreader
54
+
55
+
56
+ def test_get_current_state(
57
+ secret_reader: MagicMock,
58
+ deadmanssnitch_api: MockerFixture,
59
+ mocker: MockerFixture,
60
+ deadmanssnitch_settings: DeadMansSnitchSettingsV1,
61
+ ) -> None:
62
+ deadmanssnitch_api.get_snitches.return_value = [
63
+ Snitch(
64
+ name="prometheus.test_cluster_1.net",
65
+ token="test",
66
+ href="testc",
67
+ status="healthy",
68
+ alert_type="basic",
69
+ alert_email=["test_mail"],
70
+ interval="15_minute",
71
+ check_in_url="test_url",
72
+ tags=["app-sre"],
73
+ notes="test_notes",
74
+ )
75
+ ]
76
+ clusters = [
77
+ ClusterV1(
78
+ name="test_cluster_1",
79
+ serverUrl="testurl",
80
+ consoleUrl="test_c_url",
81
+ alertmanagerUrl="test_alert_manager",
82
+ managedClusterRoles=True,
83
+ prometheusUrl="https://prometheus.test_cluster_1.net",
84
+ enableDeadMansSnitch=True,
85
+ ),
86
+ ClusterV1(
87
+ name="test_cluster_2",
88
+ serverUrl="testurl",
89
+ consoleUrl="test_c_url",
90
+ alertmanagerUrl="test_alert_manager",
91
+ managedClusterRoles=True,
92
+ prometheusUrl="https://prometheus.test_cluster_2.net",
93
+ enableDeadMansSnitch=True,
94
+ ),
95
+ ]
96
+ mocker.patch(
97
+ "reconcile.deadmanssnitch.DeadMansSnitchIntegration.__init__"
98
+ ).return_value = None
99
+ dms_integration = DeadMansSnitchIntegration()
100
+ dms_integration._secret_reader = secret_reader
101
+ dms_integration.settings = deadmanssnitch_settings
102
+ current_state = dms_integration.get_current_state(
103
+ deadmanssnitch_api=deadmanssnitch_api,
104
+ clusters=clusters,
105
+ snitch_secret_path="test_path",
106
+ cluster_to_prometheus_mapping={
107
+ "test_cluster_1": "prometheus.test_cluster_1.net",
108
+ "test_cluster_2": "https://prometheus.test_cluster_2.net",
109
+ },
110
+ )
111
+ assert current_state["test_cluster_1"].vault_data == "secret"
112
+
113
+
114
+ def test_integration_for_create(
115
+ vault_mock: MagicMock,
116
+ deadmanssnitch_settings: DeadMansSnitchSettingsV1,
117
+ mocker: MockerFixture,
118
+ secret_reader: MagicMock,
119
+ ) -> None:
120
+ mocker.patch(
121
+ "reconcile.deadmanssnitch.DeadMansSnitchIntegration.__init__"
122
+ ).return_value = None
123
+ dms_integration = DeadMansSnitchIntegration()
124
+ dms_integration._secret_reader = secret_reader
125
+ dms_integration.settings = deadmanssnitch_settings
126
+ dms_integration.vault_client = vault_mock
127
+ mocker.patch("reconcile.deadmanssnitch.get_clusters_with_dms").return_value = [
128
+ ClusterV1(
129
+ name="create_cluster",
130
+ prometheusUrl="https://prometheus.create_cluster.devshift.net",
131
+ enableDeadMansSnitch=True,
132
+ alertmanagerUrl="alertmanager.create_cluster.devshift.net",
133
+ managedClusterRoles=True,
134
+ serverUrl="testurl",
135
+ consoleUrl="test_console",
136
+ )
137
+ ]
138
+ mocker.patch(
139
+ "reconcile.deadmanssnitch.DeadMansSnitchApi.get_snitches"
140
+ ).return_value = []
141
+ mock_create_snitch = mocker.patch(
142
+ "reconcile.deadmanssnitch.DeadMansSnitchIntegration.create_snitch"
143
+ )
144
+ mock_create_snitch.return_value = None
145
+ dms_integration.run(dry_run=False)
146
+ mock_create_snitch.assert_called_once()
147
+
148
+
149
+ def test_integration_for_delete(
150
+ deadmanssnitch_settings: DeadMansSnitchSettingsV1,
151
+ vault_mock: MagicMock,
152
+ mocker: MockerFixture,
153
+ secret_reader: MagicMock,
154
+ ) -> None:
155
+ mocker.patch(
156
+ "reconcile.deadmanssnitch.DeadMansSnitchIntegration.__init__"
157
+ ).return_value = None
158
+ dms_integration = DeadMansSnitchIntegration()
159
+ dms_integration._secret_reader = secret_reader
160
+ dms_integration.settings = deadmanssnitch_settings
161
+ dms_integration.vault_client = vault_mock
162
+ mocker.patch("reconcile.deadmanssnitch.get_clusters_with_dms").return_value = [
163
+ ClusterV1(
164
+ name="create_cluster",
165
+ prometheusUrl="https://prometheus.create_cluster.devshift.net",
166
+ enableDeadMansSnitch=False,
167
+ alertmanagerUrl="alertmanager.create_cluster.devshift.net",
168
+ managedClusterRoles=True,
169
+ serverUrl="testurl",
170
+ consoleUrl="test_console",
171
+ )
172
+ ]
173
+ mocker.patch(
174
+ "reconcile.deadmanssnitch.DeadMansSnitchApi.get_snitches"
175
+ ).return_value = [
176
+ Snitch(
177
+ name="prometheus.create_cluster.devshift.net",
178
+ token="test",
179
+ href="testc",
180
+ status="healthy",
181
+ alert_type="basic",
182
+ alert_email=["test_mail"],
183
+ interval="15_minute",
184
+ check_in_url="test_url",
185
+ tags=["app-sre"],
186
+ notes="test_notes",
187
+ )
188
+ ]
189
+ mock_delete_snitch = mocker.patch(
190
+ "reconcile.deadmanssnitch.DeadMansSnitchApi.delete_snitch"
191
+ )
192
+ mock_delete_snitch.return_value = None
193
+ dms_integration.run(dry_run=False)
194
+ mock_delete_snitch.assert_called_once_with("test")
195
+
196
+
197
+ def test_integration_for_update_vault(
198
+ deadmanssnitch_settings: DeadMansSnitchSettingsV1,
199
+ vault_mock: MagicMock,
200
+ mocker: MockerFixture,
201
+ secret_reader: MagicMock,
202
+ ) -> None:
203
+ mocker.patch(
204
+ "reconcile.deadmanssnitch.DeadMansSnitchIntegration.__init__"
205
+ ).return_value = None
206
+ dms_integration = DeadMansSnitchIntegration()
207
+ dms_integration._secret_reader = secret_reader
208
+ dms_integration.settings = deadmanssnitch_settings
209
+ dms_integration.vault_client = vault_mock
210
+ mocker.patch("reconcile.deadmanssnitch.get_clusters_with_dms").return_value = [
211
+ ClusterV1(
212
+ name="test_cluster",
213
+ prometheusUrl="https://prometheus.create_cluster.devshift.net",
214
+ enableDeadMansSnitch=True,
215
+ alertmanagerUrl="alertmanager.create_cluster.devshift.net",
216
+ managedClusterRoles=True,
217
+ serverUrl="testurl",
218
+ consoleUrl="test_console",
219
+ )
220
+ ]
221
+ mocker.patch(
222
+ "reconcile.deadmanssnitch.DeadMansSnitchApi.get_snitches"
223
+ ).return_value = [
224
+ Snitch(
225
+ name="prometheus.create_cluster.devshift.net",
226
+ token="test",
227
+ href="testc",
228
+ status="healthy",
229
+ alert_type="basic",
230
+ alert_email=["test_mail"],
231
+ interval="15_minute",
232
+ check_in_url="test_url",
233
+ tags=["app-sre"],
234
+ notes="test_notes",
235
+ )
236
+ ]
237
+ dms_integration.run(dry_run=False)
238
+ vault_mock.write.assert_called_once_with(
239
+ {
240
+ "path": deadmanssnitch_settings.snitches_path,
241
+ "data": {"deadmanssnitch-test_cluster-url": "test_url"},
242
+ },
243
+ decode_base64=False,
244
+ )
245
+
246
+
247
+ def test_integration_while_failed(
248
+ deadmanssnitch_settings: DeadMansSnitchSettingsV1,
249
+ vault_mock: MagicMock,
250
+ mocker: MockerFixture,
251
+ secret_reader: MagicMock,
252
+ ) -> None:
253
+ mocker.patch(
254
+ "reconcile.deadmanssnitch.DeadMansSnitchIntegration.__init__"
255
+ ).return_value = None
256
+ dms_integration = DeadMansSnitchIntegration()
257
+ dms_integration._secret_reader = secret_reader
258
+ dms_integration.settings = deadmanssnitch_settings
259
+ dms_integration.vault_client = vault_mock
260
+ mocker.patch("reconcile.deadmanssnitch.get_clusters_with_dms").return_value = [
261
+ ClusterV1(
262
+ name="create_cluster",
263
+ prometheusUrl="https://prometheus.create_cluster.devshift.net",
264
+ enableDeadMansSnitch=True,
265
+ alertmanagerUrl="alertmanager.create_cluster.devshift.net",
266
+ managedClusterRoles=True,
267
+ serverUrl="testurl",
268
+ consoleUrl="test_console",
269
+ )
270
+ ]
271
+ mocker.patch(
272
+ "reconcile.deadmanssnitch.DeadMansSnitchApi.get_snitches"
273
+ ).return_value = []
274
+ mocker.patch(
275
+ "reconcile.deadmanssnitch.DeadMansSnitchIntegration.create_snitch",
276
+ side_effect=Exception("mock vault"),
277
+ )
278
+ with pytest.raises(ExceptionGroup):
279
+ dms_integration.run(dry_run=False)
@@ -0,0 +1,19 @@
1
+ from typing import Optional
2
+
3
+ from reconcile.gql_definitions.common.app_interface_dms_settings import (
4
+ DeadMansSnitchSettingsV1,
5
+ query,
6
+ )
7
+ from reconcile.utils import gql
8
+ from reconcile.utils.exceptions import AppInterfaceSettingsError
9
+ from reconcile.utils.gql import GqlApi
10
+
11
+
12
+ def get_deadmanssnitch_settings(
13
+ gql_api: Optional[GqlApi] = None,
14
+ ) -> DeadMansSnitchSettingsV1:
15
+ api = gql_api if gql_api else gql.get_api()
16
+ data = query(query_func=api.query)
17
+ if data.settings and data.settings[0].dead_mans_snitch_settings is not None:
18
+ return data.settings[0].dead_mans_snitch_settings
19
+ raise AppInterfaceSettingsError("deadmanssnitch settings missing")
@@ -0,0 +1,18 @@
1
+ from typing import Optional
2
+
3
+ from reconcile.gql_definitions.common.clusters_with_dms import (
4
+ ClusterV1,
5
+ query,
6
+ )
7
+ from reconcile.utils import gql
8
+ from reconcile.utils.gql import GqlApi
9
+
10
+
11
+ def get_clusters_with_dms(
12
+ gql_api: Optional[GqlApi] = None,
13
+ ) -> list[ClusterV1]:
14
+ # get the clusters containing the filed enableDeadMansSnitch
15
+ variable = {"filter": {"enableDeadMansSnitch": {"ne": None}}}
16
+ api = gql_api if gql_api else gql.get_api()
17
+ data = query(query_func=api.query, variables=variable)
18
+ return data.clusters or []
@@ -0,0 +1,85 @@
1
+ import logging
2
+ from typing import (
3
+ Any,
4
+ Optional,
5
+ Self,
6
+ )
7
+
8
+ import requests
9
+ from pydantic import BaseModel
10
+
11
+ BASE_URL = "https://api.deadmanssnitch.com/v1/snitches"
12
+ REQUEST_TIMEOUT = 60
13
+
14
+
15
+ class DeadManssnitchException(Exception):
16
+ pass
17
+
18
+
19
+ class Snitch(BaseModel):
20
+ token: str
21
+ href: str
22
+ name: str
23
+ tags: list[str]
24
+ notes: str
25
+ status: str
26
+ check_in_url: str
27
+ interval: str
28
+ alert_type: str
29
+ alert_email: list[str]
30
+ vault_data: Optional[str]
31
+
32
+ def needs_vault_update(self) -> bool:
33
+ return self.vault_data is not None and self.check_in_url != self.vault_data
34
+
35
+
36
+ class DeadMansSnitchApi:
37
+ def __init__(
38
+ self, token: str, url: str = BASE_URL, timeout: int = REQUEST_TIMEOUT
39
+ ) -> None:
40
+ self.token = token
41
+ self.url = url
42
+ self.timeout = timeout
43
+ self.session = requests.Session()
44
+
45
+ def __enter__(self) -> Self:
46
+ return self
47
+
48
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
49
+ self.session.close()
50
+
51
+ def get_snitches(self, tags: list[str]) -> list[Snitch]:
52
+ full_url = f"{self.url}?tags={','.join(tags)}"
53
+ logging.debug("Getting snitches for tags:%s", tags)
54
+ response = self.session.get(
55
+ url=full_url, auth=(self.token, ""), timeout=self.timeout
56
+ )
57
+ response.raise_for_status()
58
+ snitches = [Snitch(**item) for item in response.json()]
59
+ return snitches
60
+
61
+ def create_snitch(self, payload: dict) -> Snitch:
62
+ if payload.get("name") is None or payload.get("interval") is None:
63
+ raise DeadManssnitchException(
64
+ "Invalid payload,name and interval are mandatory"
65
+ )
66
+ headers = {"Content-Type": "application/json"}
67
+ logging.debug("Creating new snitch with name:: %s ", payload["name"])
68
+ response = self.session.post(
69
+ url=self.url,
70
+ json=payload,
71
+ auth=(self.token, ""),
72
+ headers=headers,
73
+ timeout=self.timeout,
74
+ )
75
+ response.raise_for_status()
76
+ response_json = response.json()
77
+ return Snitch(**response_json)
78
+
79
+ def delete_snitch(self, token: str) -> None:
80
+ delete_api_url = f"{self.url}/{token}"
81
+ response = self.session.delete(
82
+ url=delete_api_url, auth=(self.token, ""), timeout=self.timeout
83
+ )
84
+ response.raise_for_status()
85
+ logging.debug("Successfully deleted snich: %s", token)