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.
- {qontract_reconcile-0.10.2.dev19.dist-info → qontract_reconcile-0.10.2.dev21.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.2.dev19.dist-info → qontract_reconcile-0.10.2.dev21.dist-info}/RECORD +8 -8
- reconcile/external_resources/integration.py +9 -4
- reconcile/external_resources/manager.py +40 -8
- reconcile/external_resources/model.py +11 -0
- reconcile/utils/dynatrace/client.py +31 -0
- {qontract_reconcile-0.10.2.dev19.dist-info → qontract_reconcile-0.10.2.dev21.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev19.dist-info → qontract_reconcile-0.10.2.dev21.dist-info}/entry_points.txt +0 -0
{qontract_reconcile-0.10.2.dev19.dist-info → qontract_reconcile-0.10.2.dev21.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.2.
|
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
|
{qontract_reconcile-0.10.2.dev19.dist-info → qontract_reconcile-0.10.2.dev21.dist-info}/RECORD
RENAMED
@@ -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=
|
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=
|
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=
|
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=
|
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.
|
770
|
-
qontract_reconcile-0.10.2.
|
771
|
-
qontract_reconcile-0.10.2.
|
772
|
-
qontract_reconcile-0.10.2.
|
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=
|
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.
|
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)
|
{qontract_reconcile-0.10.2.dev19.dist-info → qontract_reconcile-0.10.2.dev21.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|