qontract-reconcile 0.10.2.dev19__py3-none-any.whl → 0.10.2.dev21__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.4
2
2
  Name: qontract-reconcile
3
- Version: 0.10.2.dev19
3
+ Version: 0.10.2.dev21
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Project-URL: homepage, https://github.com/app-sre/qontract-reconcile
6
6
  Project-URL: repository, https://github.com/app-sre/qontract-reconcile
@@ -197,12 +197,12 @@ reconcile/endpoints_discovery/merge_request_manager.py,sha256=wUMsumxv8RnWaRatta
197
197
  reconcile/external_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
198
198
  reconcile/external_resources/aws.py,sha256=NSaOeHqFEcMaMxNjJwuQZosolgsJ8XRVvwkEEBj9vrw,7730
199
199
  reconcile/external_resources/factories.py,sha256=TyJMaijDfPIFYks9i6dhKN7nSR1BoCkoBs1iPExKpcE,5493
200
- reconcile/external_resources/integration.py,sha256=gBVO5dE8JyZ3xYcYik-MTIp_18oU7_hpYc_oztyfElQ,6753
200
+ reconcile/external_resources/integration.py,sha256=JF38M7R0Z4ADUTx57TZqSZH9k_xpPlbAxQAcGyIISuM,6925
201
201
  reconcile/external_resources/integration_secrets_sync.py,sha256=dX09O3r6KURziUYYfiki10orNjOGVma-XojhVqd0ww4,1667
202
- reconcile/external_resources/manager.py,sha256=eVaaGCaKDkc897xt5cA5-B4yYuS9VWR-Z7Uom0uSsG0,15971
202
+ reconcile/external_resources/manager.py,sha256=q7Ezp-g3MhpH_t8zpNMH5wCzHvh-nERpHzvBPA1G1ng,17031
203
203
  reconcile/external_resources/meta.py,sha256=noaytFzmShpzLA_ebGh7wuP45mOfHIOnnoUxivjDa1I,672
204
204
  reconcile/external_resources/metrics.py,sha256=KiBjMUaN_z0cSkF_7Ar_a8RiuiwVqjyMcVdISlxhzXE,3898
205
- reconcile/external_resources/model.py,sha256=YJylbAhetN9szpLUFd9jFqxCRMvSWXVxSC9OMQNV-wg,11316
205
+ reconcile/external_resources/model.py,sha256=KdB3eYlopWOLQmvL1aICjm-kAPIlY2N_zSikcCFBQpk,11751
206
206
  reconcile/external_resources/reconciler.py,sha256=K9QvbQCIOCuOHnPIxQE_P_jFtrkF3dGo8d_cCCh08Ys,8973
207
207
  reconcile/external_resources/secrets_sync.py,sha256=50fK4fzgSz-K8uy5_DQQWA_ju_rTDYAC2HRymgfY7TA,16344
208
208
  reconcile/external_resources/state.py,sha256=ye8yjMoCtTHSRhDH7skFLDIHIuYTjisWYCTJrwnmbEw,9565
@@ -657,7 +657,7 @@ reconcile/utils/clusterhealth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
657
657
  reconcile/utils/clusterhealth/providerbase.py,sha256=DXomGYogckBLqWtXn0PXU0hWYxB6K0F7ernldrkHhVY,1140
658
658
  reconcile/utils/clusterhealth/telemeter.py,sha256=PllSLsJXvGNatmTF4mxCNPVbDrpr_MPk0m5pWj-LT6g,1534
659
659
  reconcile/utils/dynatrace/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
660
- reconcile/utils/dynatrace/client.py,sha256=H8EjqmZlB1a2ionAjV8_R1ozs9lWbmPCYKLe0J8kZAs,2838
660
+ reconcile/utils/dynatrace/client.py,sha256=RUk6KH-3CJyfJ1jolrdGQR4Hhz-tIWWJo9dsZ1IgJVw,3736
661
661
  reconcile/utils/glitchtip/__init__.py,sha256=FT6iBhGqoe7KExFdbgL8AYUb64iW_4snF5__Dcl7yt0,258
662
662
  reconcile/utils/glitchtip/client.py,sha256=ovh4tx-ajlihjvcq6nyY4chulbuMJYvzDPv9j9CuAKM,7867
663
663
  reconcile/utils/glitchtip/models.py,sha256=AJuGq4_A6G_T7asBKIw69-fOZLmT8HFrTKBEys7Tp00,6481
@@ -766,7 +766,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
766
766
  tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
767
767
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
768
768
  tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
769
- qontract_reconcile-0.10.2.dev19.dist-info/METADATA,sha256=wb_IQ6QESeh7TYBGvXCUJde_kGmYx8m3jIPVO7MsETA,24665
770
- qontract_reconcile-0.10.2.dev19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
771
- qontract_reconcile-0.10.2.dev19.dist-info/entry_points.txt,sha256=JniHZPadNOILPyfSl0LF2YSp3Db7K2_W2CN7i9f3Gos,540
772
- qontract_reconcile-0.10.2.dev19.dist-info/RECORD,,
769
+ qontract_reconcile-0.10.2.dev21.dist-info/METADATA,sha256=odjOqT6xlL94VSczTzQ6DNXDbhrppOG_UhaZk9wNnbg,24665
770
+ qontract_reconcile-0.10.2.dev21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
771
+ qontract_reconcile-0.10.2.dev21.dist-info/entry_points.txt,sha256=JniHZPadNOILPyfSl0LF2YSp3Db7K2_W2CN7i9f3Gos,540
772
+ qontract_reconcile-0.10.2.dev21.dist-info/RECORD,,
@@ -3,6 +3,7 @@ from collections.abc import Callable
3
3
  from typing import Any
4
4
 
5
5
  from reconcile.external_resources.manager import (
6
+ ExternalResourceDryRunsValidator,
6
7
  ExternalResourcesInventory,
7
8
  ExternalResourcesManager,
8
9
  setup_factories,
@@ -89,6 +90,10 @@ def create_er_manager(
89
90
  m_inventory = load_module_inventory(get_modules())
90
91
  namespaces = [ns for ns in get_namespaces() if ns.external_resources]
91
92
  er_inventory = ExternalResourcesInventory(namespaces)
93
+ state_manager = ExternalResourcesStateDynamoDB(
94
+ aws_api=aws_api,
95
+ table_name=er_settings.state_dynamodb_table,
96
+ )
92
97
 
93
98
  if not workers_cluster:
94
99
  workers_cluster = er_settings.workers_cluster.name
@@ -104,10 +109,7 @@ def create_er_manager(
104
109
  ),
105
110
  er_inventory=er_inventory,
106
111
  module_inventory=m_inventory,
107
- state_manager=ExternalResourcesStateDynamoDB(
108
- aws_api=aws_api,
109
- table_name=er_settings.state_dynamodb_table,
110
- ),
112
+ state_manager=state_manager,
111
113
  reconciler=K8sExternalResourcesReconciler(
112
114
  controller=build_job_controller(
113
115
  integration=QONTRACT_INTEGRATION,
@@ -128,6 +130,9 @@ def create_er_manager(
128
130
  thread_pool_size=thread_pool_size,
129
131
  dry_run=dry_run,
130
132
  ),
133
+ dry_runs_validator=ExternalResourceDryRunsValidator(
134
+ state_manager, er_inventory
135
+ ),
131
136
  )
132
137
 
133
138
 
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from collections import Counter
2
3
  from collections.abc import Iterable
3
4
  from datetime import UTC, datetime
4
5
 
@@ -19,6 +20,7 @@ from reconcile.external_resources.model import (
19
20
  ExternalResourceKey,
20
21
  ExternalResourceModuleConfiguration,
21
22
  ExternalResourceOrphanedResourcesError,
23
+ ExternalResourceOutputResourceNameDuplications,
22
24
  ExternalResourcesInventory,
23
25
  ExternalResourceValidationError,
24
26
  ModuleInventory,
@@ -73,6 +75,41 @@ def setup_factories(
73
75
  return of
74
76
 
75
77
 
78
+ class ExternalResourceDryRunsValidator:
79
+ def __init__(
80
+ self,
81
+ state_manager: ExternalResourcesStateDynamoDB,
82
+ er_inventory: ExternalResourcesInventory,
83
+ ):
84
+ self.state_mgr = state_manager
85
+ self.er_inventory = er_inventory
86
+
87
+ def _check_output_resource_name_duplications(
88
+ self,
89
+ ) -> None:
90
+ specs = Counter(
91
+ (
92
+ spec.cluster_name,
93
+ spec.namespace_name,
94
+ spec.output_resource_name,
95
+ )
96
+ for spec in self.er_inventory.values()
97
+ )
98
+ if duplicates := [key for key, count in specs.items() if count > 1]:
99
+ raise ExternalResourceOutputResourceNameDuplications(duplicates)
100
+
101
+ def _check_orphaned_objects(self) -> None:
102
+ state_keys = self.state_mgr.get_all_resource_keys()
103
+ inventory_keys = set(self.er_inventory.keys())
104
+ orphans = state_keys - inventory_keys
105
+ if len(orphans) > 0:
106
+ raise ExternalResourceOrphanedResourcesError(orphans)
107
+
108
+ def validate(self) -> None:
109
+ self._check_orphaned_objects()
110
+ self._check_output_resource_name_duplications()
111
+
112
+
76
113
  class ExternalResourcesManager:
77
114
  def __init__(
78
115
  self,
@@ -84,6 +121,7 @@ class ExternalResourcesManager:
84
121
  er_inventory: ExternalResourcesInventory,
85
122
  factories: ObjectFactory[ExternalResourceFactory],
86
123
  secrets_reconciler: InClusterSecretsReconciler,
124
+ dry_runs_validator: ExternalResourceDryRunsValidator,
87
125
  thread_pool_size: int,
88
126
  ) -> None:
89
127
  self.state_mgr = state_manager
@@ -96,6 +134,7 @@ class ExternalResourcesManager:
96
134
  self.secrets_reconciler = secrets_reconciler
97
135
  self.errors: dict[ExternalResourceKey, ExternalResourceValidationError] = {}
98
136
  self.thread_pool_size = thread_pool_size
137
+ self.dry_runs_validator = dry_runs_validator
99
138
 
100
139
  def _get_reconcile_action(
101
140
  self, reconciliation: Reconciliation, state: ExternalResourceState
@@ -199,13 +238,6 @@ class ExternalResourcesManager:
199
238
  to_reconcile.add(r)
200
239
  return to_reconcile
201
240
 
202
- def _check_orphaned_objects(self) -> None:
203
- state_keys = self.state_mgr.get_all_resource_keys()
204
- inventory_keys = set(self.er_inventory.keys())
205
- orphans = state_keys - inventory_keys
206
- if len(orphans) > 0:
207
- raise ExternalResourceOrphanedResourcesError(orphans)
208
-
209
241
  def _get_reconciliation_status(
210
242
  self,
211
243
  r: Reconciliation,
@@ -374,7 +406,7 @@ class ExternalResourcesManager:
374
406
  self._sync_secrets(to_sync_keys=to_sync_keys | pending_sync_keys)
375
407
 
376
408
  def handle_dry_run_resources(self) -> None:
377
- self._check_orphaned_objects()
409
+ self.dry_runs_validator.validate()
378
410
  desired_r = self._get_desired_objects_reconciliations()
379
411
  deleted_r = self._get_deleted_objects_reconciliations()
380
412
  reconciliations = desired_r.union(deleted_r)
@@ -45,6 +45,17 @@ class ExternalResourceOrphanedResourcesError(Exception):
45
45
  super().__init__("".join(msg))
46
46
 
47
47
 
48
+ class ExternalResourceOutputResourceNameDuplications(Exception):
49
+ def __init__(self, duplicates: Iterable[tuple[str, str, str]]) -> None:
50
+ msg = [
51
+ "There are output_resource_name attribute duplications. ",
52
+ "output_resource_name must be unique within a cluster/namespace.\n"
53
+ "Duplications:\n",
54
+ "\n".join(map(str, duplicates)),
55
+ ]
56
+ super().__init__("".join(msg))
57
+
58
+
48
59
  class ExternalResourceValidationError(Exception):
49
60
  errors: list[str] = []
50
61
 
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Iterable
4
+ from datetime import datetime
5
+ from unittest.mock import patch
4
6
 
5
7
  from dynatrace import Dynatrace
6
8
  from dynatrace.environment_v2.tokens_api import ApiTokenUpdate
@@ -33,11 +35,32 @@ class DynatraceAPIToken(BaseModel):
33
35
  scopes: list[str]
34
36
 
35
37
 
38
+ # TODO: Remove once APPSRE-11428 is resolved #######
39
+ ISO_8601 = "%Y-%m-%dT%H:%M:%S.%fZ"
40
+ FIXED_ISO_8601 = "%Y-%m-%dT%H:%M:%SZ"
41
+
42
+
43
+ def custom_iso8601_to_datetime(timestamp: str | None) -> datetime | None:
44
+ if isinstance(timestamp, str):
45
+ try:
46
+ return datetime.strptime(timestamp, ISO_8601)
47
+ except ValueError:
48
+ return datetime.strptime(timestamp, FIXED_ISO_8601)
49
+ return timestamp
50
+
51
+
52
+ ################################################
53
+
54
+
36
55
  class DynatraceClient:
37
56
  def __init__(self, environment_url: str, api: Dynatrace) -> None:
38
57
  self._environment_url = environment_url
39
58
  self._api = api
40
59
 
60
+ @patch(
61
+ "dynatrace.environment_v2.tokens_api.iso8601_to_datetime",
62
+ custom_iso8601_to_datetime,
63
+ )
41
64
  def create_api_token(
42
65
  self, name: str, scopes: Iterable[str]
43
66
  ) -> DynatraceAPITokenCreated:
@@ -49,6 +72,10 @@ class DynatraceClient:
49
72
  ) from e
50
73
  return DynatraceAPITokenCreated(token=token.token, id=token.id)
51
74
 
75
+ @patch(
76
+ "dynatrace.environment_v2.tokens_api.iso8601_to_datetime",
77
+ custom_iso8601_to_datetime,
78
+ )
52
79
  def get_token_ids_map_for_name_prefix(self, prefix: str) -> dict[str, str]:
53
80
  try:
54
81
  dt_tokens = self._api.tokens.list()
@@ -60,6 +87,10 @@ class DynatraceClient:
60
87
  token.id: token.name for token in dt_tokens if token.name.startswith(prefix)
61
88
  }
62
89
 
90
+ @patch(
91
+ "dynatrace.environment_v2.tokens_api.iso8601_to_datetime",
92
+ custom_iso8601_to_datetime,
93
+ )
63
94
  def get_token_by_id(self, token_id: str) -> DynatraceAPIToken:
64
95
  try:
65
96
  token = self._api.tokens.get(token_id=token_id)