qontract-reconcile 0.10.1rc944__py3-none-any.whl → 0.10.1rc947__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.1rc944
3
+ Version: 0.10.1rc947
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
@@ -179,8 +179,10 @@ reconcile/cna/assets/asset.py,sha256=KWgA4fuDAEGsJwmR52WwK_YgSJMW-1cV2la3lmNf4iE
179
179
  reconcile/cna/assets/asset_factory.py,sha256=7T7X_J6xIsoGETqBRI45_EyIKEdQcnRPt_GAuVuLQcc,785
180
180
  reconcile/cna/assets/null.py,sha256=85mVh97atCoC0aLuX47poTZiyOthmziJeBsUw0c924w,1658
181
181
  reconcile/dynatrace_token_provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
182
- reconcile/dynatrace_token_provider/integration.py,sha256=Tt3Nayf1TOdizBStXdEaJKfKqlOkJljw22lWlN2GB2s,16518
182
+ reconcile/dynatrace_token_provider/dependencies.py,sha256=uJLvR48kxfqjnBuP60XQx5RbkWPL3__FCgWjqwhKEjo,2160
183
+ reconcile/dynatrace_token_provider/integration.py,sha256=Tt7TxLqhEZW0bpnNVMDwf8ATZJqviYBaPCw-jO4So9Y,14712
183
184
  reconcile/dynatrace_token_provider/metrics.py,sha256=xiKkl8fTEBQaXJelGCPNTZhHAWdO1M3pCXNr_Tei63c,1285
185
+ reconcile/dynatrace_token_provider/ocm.py,sha256=IwksRMyGcJnamV88ORlBoyOr7uRENhMaHBoSXaGfwDY,2784
184
186
  reconcile/external_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
185
187
  reconcile/external_resources/aws.py,sha256=JvjKaABy2Pg8u8Lq82Acv4zMvpE3_qGKes7OG-zlHOM,2956
186
188
  reconcile/external_resources/factories.py,sha256=DXgaLxoO87zZ76VOpRpu2GeYGhsbfOnOx5mrzgo4Gf4,4767
@@ -842,8 +844,8 @@ tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jr
842
844
  tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
843
845
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
844
846
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
845
- qontract_reconcile-0.10.1rc944.dist-info/METADATA,sha256=yw4MzNhOPePhmbYezNBZc4Am5fee8W0iw-osoD7Srh4,2262
846
- qontract_reconcile-0.10.1rc944.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
847
- qontract_reconcile-0.10.1rc944.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
848
- qontract_reconcile-0.10.1rc944.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
849
- qontract_reconcile-0.10.1rc944.dist-info/RECORD,,
847
+ qontract_reconcile-0.10.1rc947.dist-info/METADATA,sha256=QNbbPwtFuuLOZcxaB5ZtfE7CwXA1JNufPkLI1Lu6h6U,2262
848
+ qontract_reconcile-0.10.1rc947.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
849
+ qontract_reconcile-0.10.1rc947.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
850
+ qontract_reconcile-0.10.1rc947.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
851
+ qontract_reconcile-0.10.1rc947.dist-info/RECORD,,
@@ -0,0 +1,54 @@
1
+ from reconcile.dynatrace_token_provider.ocm import OCMClient
2
+ from reconcile.typed_queries.dynatrace_environments import get_dynatrace_environments
3
+ from reconcile.typed_queries.ocm import get_ocm_environments
4
+ from reconcile.utils.dynatrace.client import DynatraceClient
5
+ from reconcile.utils.ocm_base_client import (
6
+ init_ocm_base_client,
7
+ )
8
+ from reconcile.utils.secret_reader import SecretReaderBase
9
+
10
+
11
+ class Dependencies:
12
+ """
13
+ Depenedencies class to hold all the dependencies (API clients) for the Dynatrace Token Provider.
14
+ Dependency inversion simplifies setting up tests.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ secret_reader: SecretReaderBase,
20
+ dynatrace_client_by_tenant_id: dict[str, DynatraceClient],
21
+ ocm_client_by_env_name: dict[str, OCMClient],
22
+ ):
23
+ self.secret_reader = secret_reader
24
+ self.dynatrace_client_by_tenant_id: dict[str, DynatraceClient] = (
25
+ dynatrace_client_by_tenant_id
26
+ )
27
+ self.ocm_client_by_env_name: dict[str, OCMClient] = ocm_client_by_env_name
28
+
29
+ def populate(self) -> None:
30
+ self._populate_dynatrace_client_map()
31
+ self._populate_ocm_clients()
32
+
33
+ def _populate_dynatrace_client_map(self) -> None:
34
+ dynatrace_environments = get_dynatrace_environments()
35
+ if not dynatrace_environments:
36
+ raise RuntimeError("No Dynatrace environment defined.")
37
+ for tenant in dynatrace_environments:
38
+ dt_api_token = self.secret_reader.read_secret(tenant.bootstrap_token)
39
+ dt_client = DynatraceClient.create(
40
+ environment_url=tenant.environment_url,
41
+ token=dt_api_token,
42
+ api=None,
43
+ )
44
+ tenant_id = tenant.environment_url.split(".")[0].removeprefix("https://")
45
+ self.dynatrace_client_by_tenant_id[tenant_id] = dt_client
46
+
47
+ def _populate_ocm_clients(self) -> None:
48
+ ocm_environments = get_ocm_environments()
49
+ self.ocm_client_by_env_name = {
50
+ env.name: OCMClient(
51
+ ocm_client=init_ocm_base_client(env, self.secret_reader)
52
+ )
53
+ for env in ocm_environments
54
+ }
@@ -1,56 +1,29 @@
1
1
  import base64
2
2
  import logging
3
- import sys
4
3
  from collections.abc import Iterable, Mapping
5
4
  from datetime import timedelta
6
5
  from typing import Any
7
6
 
8
- from dynatrace import Dynatrace
9
- from dynatrace.environment_v2.tokens_api import ApiTokenCreated
10
-
7
+ from reconcile.dynatrace_token_provider.dependencies import Dependencies
11
8
  from reconcile.dynatrace_token_provider.metrics import (
12
9
  DTPClustersManagedGauge,
13
10
  DTPOrganizationErrorRate,
14
11
  )
15
- from reconcile.gql_definitions.common.ocm_environments import (
16
- query as ocm_environment_query,
17
- )
18
- from reconcile.gql_definitions.dynatrace_token_provider import (
19
- dynatrace_bootstrap_tokens,
20
- )
21
- from reconcile.gql_definitions.dynatrace_token_provider.dynatrace_bootstrap_tokens import (
22
- DynatraceEnvironmentQueryData,
23
- )
24
- from reconcile.gql_definitions.fragments.ocm_environment import OCMEnvironment
12
+ from reconcile.dynatrace_token_provider.ocm import Cluster, OCMClient
25
13
  from reconcile.utils import (
26
- gql,
27
14
  metrics,
28
15
  )
16
+ from reconcile.utils.dynatrace.client import DynatraceAPITokenCreated, DynatraceClient
29
17
  from reconcile.utils.ocm.base import (
30
18
  OCMClusterServiceLogCreateModel,
31
19
  OCMServiceLogSeverity,
32
20
  )
33
- from reconcile.utils.ocm.clusters import (
34
- ClusterDetails,
35
- discover_clusters_by_labels,
36
- )
37
21
  from reconcile.utils.ocm.labels import subscription_label_filter
38
- from reconcile.utils.ocm.service_log import create_service_log
39
22
  from reconcile.utils.ocm.sre_capability_labels import sre_capability_label_key
40
- from reconcile.utils.ocm.syncsets import (
41
- create_syncset,
42
- get_syncset,
43
- patch_syncset,
44
- )
45
- from reconcile.utils.ocm_base_client import (
46
- OCMBaseClient,
47
- init_ocm_base_client,
48
- )
49
23
  from reconcile.utils.runtime.integration import (
50
24
  PydanticRunParams,
51
25
  QontractReconcileIntegration,
52
26
  )
53
- from reconcile.utils.secret_reader import SecretReaderBase
54
27
 
55
28
  QONTRACT_INTEGRATION = "dynatrace-token-provider"
56
29
  SYNCSET_ID = "ext-dynatrace-tokens-dtp"
@@ -81,12 +54,19 @@ class DynatraceTokenProviderIntegration(
81
54
  return QONTRACT_INTEGRATION
82
55
 
83
56
  def run(self, dry_run: bool) -> None:
57
+ dependencies = Dependencies(
58
+ secret_reader=self.secret_reader,
59
+ dynatrace_client_by_tenant_id={},
60
+ ocm_client_by_env_name={},
61
+ )
62
+ dependencies.populate()
63
+ self.reconcile(dry_run=dry_run, dependencies=dependencies)
64
+
65
+ def reconcile(self, dry_run: bool, dependencies: Dependencies) -> None:
84
66
  with metrics.transactional_metrics(self.name):
85
67
  unhandled_exceptions = []
86
- for env in self.get_ocm_environments():
87
- ocm_client = init_ocm_base_client(env, self.secret_reader)
88
- clusters = discover_clusters_by_labels(
89
- ocm_api=ocm_client,
68
+ for ocm_env_name, ocm_client in dependencies.ocm_client_by_env_name.items():
69
+ clusters = ocm_client.discover_clusters_by_labels(
90
70
  label_filter=subscription_label_filter().like(
91
71
  "key", dtp_label_key("%")
92
72
  ),
@@ -94,7 +74,7 @@ class DynatraceTokenProviderIntegration(
94
74
  metrics.set_gauge(
95
75
  DTPClustersManagedGauge(
96
76
  integration=self.name,
97
- ocm_env=env.name,
77
+ ocm_env=ocm_env_name,
98
78
  ),
99
79
  len(clusters),
100
80
  )
@@ -106,7 +86,6 @@ class DynatraceTokenProviderIntegration(
106
86
  for cluster in clusters
107
87
  if cluster.organization_id in self.params.ocm_organization_ids
108
88
  ]
109
- dt_clients = self.get_all_dynatrace_clients(self.secret_reader)
110
89
  dtp_tenant_label_key = f"{dtp_label_key(None)}.tenant"
111
90
  existing_dtp_tokens = {}
112
91
 
@@ -114,87 +93,60 @@ class DynatraceTokenProviderIntegration(
114
93
  try:
115
94
  with DTPOrganizationErrorRate(
116
95
  integration=self.name,
117
- ocm_env=env.name,
96
+ ocm_env=ocm_env_name,
118
97
  org_id=cluster.organization_id,
119
98
  ):
120
- tenant_id = cluster.labels.get_label_value(
121
- dtp_tenant_label_key
122
- )
99
+ tenant_id = cluster.dt_tenant
123
100
  if not tenant_id:
124
101
  _expose_errors_as_service_log(
125
102
  ocm_client,
126
- cluster_uuid=cluster.ocm_cluster.external_id,
103
+ cluster_uuid=cluster.external_id,
127
104
  error=f"Missing label {dtp_tenant_label_key}",
128
105
  )
129
106
  continue
130
- if tenant_id not in dt_clients:
107
+ if (
108
+ tenant_id
109
+ not in dependencies.dynatrace_client_by_tenant_id
110
+ ):
131
111
  _expose_errors_as_service_log(
132
112
  ocm_client,
133
- cluster_uuid=cluster.ocm_cluster.external_id,
113
+ cluster_uuid=cluster.external_id,
134
114
  error=f"Dynatrace tenant {tenant_id} does not exist",
135
115
  )
136
116
  continue
117
+ dt_client = dependencies.dynatrace_client_by_tenant_id[
118
+ tenant_id
119
+ ]
137
120
 
138
121
  if tenant_id not in existing_dtp_tokens:
139
122
  existing_dtp_tokens[tenant_id] = (
140
- self.get_all_dtp_tokens(dt_clients[tenant_id])
123
+ dt_client.get_token_ids_for_name_prefix(
124
+ prefix="dtp-"
125
+ )
141
126
  )
142
127
 
143
128
  self.process_cluster(
144
129
  dry_run,
145
130
  cluster,
146
- dt_clients[tenant_id],
131
+ dt_client,
147
132
  ocm_client,
148
133
  existing_dtp_tokens[tenant_id],
149
134
  tenant_id,
150
135
  )
151
136
  except Exception as e:
152
137
  unhandled_exceptions.append(
153
- f"{env}/{cluster.organization_id}/{cluster.ocm_cluster.external_id}: {e}"
138
+ f"{ocm_env_name}/{cluster.organization_id}/{cluster.external_id}: {e}"
154
139
  )
155
140
 
156
141
  if unhandled_exceptions:
157
142
  raise ReconcileErrorSummary(unhandled_exceptions)
158
- sys.exit(0)
159
-
160
- def get_ocm_environments(self) -> list[OCMEnvironment]:
161
- return ocm_environment_query(gql.get_api().query).environments
162
-
163
- def get_all_dynatrace_tenants(self) -> DynatraceEnvironmentQueryData:
164
- dt_tenants = dynatrace_bootstrap_tokens.query(query_func=gql.get_api().query)
165
- return dt_tenants
166
-
167
- def get_all_dynatrace_clients(
168
- self, secret_reader: SecretReaderBase
169
- ) -> dict[str, Dynatrace]:
170
- dt_tenants = self.get_all_dynatrace_tenants()
171
- dynatrace_clients = {}
172
- if not dt_tenants.environments:
173
- raise RuntimeError("No Dynatrace environment defined.")
174
- for tenant in dt_tenants.environments:
175
- dt_bootstrap_token = secret_reader.read_secret(tenant.bootstrap_token)
176
- dt_client = Dynatrace(
177
- tenant.environment_url,
178
- dt_bootstrap_token,
179
- )
180
- tenant_id = tenant.environment_url.split(".")[0].removeprefix("https://")
181
- dynatrace_clients[tenant_id] = dt_client
182
- return dynatrace_clients
183
-
184
- def get_all_dtp_tokens(self, dt_client: Dynatrace) -> list[str]:
185
- try:
186
- dt_tokens = dt_client.tokens.list()
187
- except Exception as e:
188
- logging.error("Failed to retrieve all dtp tokens")
189
- raise e
190
- return [token.id for token in dt_tokens if token.name.startswith("dtp-")]
191
143
 
192
144
  def process_cluster(
193
145
  self,
194
146
  dry_run: bool,
195
- cluster: ClusterDetails,
196
- dt_client: Dynatrace,
197
- ocm_client: OCMBaseClient,
147
+ cluster: Cluster,
148
+ dt_client: DynatraceClient,
149
+ ocm_client: OCMClient,
198
150
  existing_dtp_tokens: Iterable[str],
199
151
  tenant_id: str,
200
152
  ) -> None:
@@ -204,11 +156,10 @@ class DynatraceTokenProviderIntegration(
204
156
  if not dry_run:
205
157
  try:
206
158
  (ingestion_token, operator_token) = self.create_dynatrace_tokens(
207
- dt_client, cluster.ocm_cluster.external_id
159
+ dt_client, cluster.external_id
208
160
  )
209
- create_syncset(
210
- ocm_client,
211
- cluster.ocm_cluster.id,
161
+ ocm_client.create_syncset(
162
+ cluster.id,
212
163
  self.construct_syncset(
213
164
  ingestion_token, operator_token, dt_api_url
214
165
  ),
@@ -216,46 +167,46 @@ class DynatraceTokenProviderIntegration(
216
167
  except Exception as e:
217
168
  _expose_errors_as_service_log(
218
169
  ocm_client,
219
- cluster.ocm_cluster.external_id,
170
+ cluster.external_id,
220
171
  f"DTP can't create Syncset with the tokens {str(e.args)}",
221
172
  )
222
173
  logging.info(
223
- f"Ingestion and operator tokens created in Dynatrace for cluster {cluster.ocm_cluster.external_id}."
174
+ f"Ingestion and operator tokens created in Dynatrace for cluster {cluster.external_id}."
224
175
  )
225
176
  logging.info(
226
- f"SyncSet {SYNCSET_ID} created in cluster {cluster.ocm_cluster.external_id}."
177
+ f"SyncSet {SYNCSET_ID} created in cluster {cluster.external_id}."
227
178
  )
228
179
  else:
229
180
  tokens = self.get_tokens_from_syncset(existing_syncset)
230
181
  need_patching = False
231
182
  for token_name, token in tokens.items():
232
- if token["id"] not in existing_dtp_tokens:
183
+ if token.id not in existing_dtp_tokens:
233
184
  need_patching = True
234
185
  logging.info(f"{token_name} missing in Dynatrace.")
235
186
  if token_name == DYNATRACE_INGESTION_TOKEN_NAME:
236
187
  if not dry_run:
237
188
  ingestion_token = self.create_dynatrace_ingestion_token(
238
- dt_client, cluster.ocm_cluster.external_id
189
+ dt_client, cluster.external_id
239
190
  )
240
- token["id"] = ingestion_token.id
241
- token["token"] = ingestion_token.token
191
+ token.id = ingestion_token.id
192
+ token.token = ingestion_token.token
242
193
  logging.info(
243
- f"Ingestion token created in Dynatrace for cluster {cluster.ocm_cluster.external_id}."
194
+ f"Ingestion token created in Dynatrace for cluster {cluster.external_id}."
244
195
  )
245
196
  elif token_name == DYNATRACE_OPERATOR_TOKEN_NAME:
246
197
  if not dry_run:
247
198
  operator_token = self.create_dynatrace_operator_token(
248
- dt_client, cluster.ocm_cluster.external_id
199
+ dt_client, cluster.external_id
249
200
  )
250
- token["id"] = operator_token.id
251
- token["token"] = operator_token.token
201
+ token.id = operator_token.id
202
+ token.token = operator_token.token
252
203
  logging.info(
253
- f"Operator token created in Dynatrace for cluster {cluster.ocm_cluster.external_id}."
204
+ f"Operator token created in Dynatrace for cluster {cluster.external_id}."
254
205
  )
255
206
  elif token_name == DYNATRACE_INGESTION_TOKEN_NAME:
256
- ingestion_token = ApiTokenCreated(raw_element=token)
207
+ ingestion_token = token
257
208
  elif token_name == DYNATRACE_OPERATOR_TOKEN_NAME:
258
- operator_token = ApiTokenCreated(raw_element=token)
209
+ operator_token = token
259
210
  if need_patching:
260
211
  if not dry_run:
261
212
  patch_syncset_payload = self.construct_base_syncset(
@@ -265,25 +216,22 @@ class DynatraceTokenProviderIntegration(
265
216
  )
266
217
  try:
267
218
  logging.info(f"Patching syncset {SYNCSET_ID}.")
268
- patch_syncset(
269
- ocm_client,
270
- cluster_id=cluster.ocm_cluster.id,
219
+ ocm_client.patch_syncset(
220
+ cluster_id=cluster.id,
271
221
  syncset_id=SYNCSET_ID,
272
222
  syncset_map=patch_syncset_payload,
273
223
  )
274
224
  except Exception as e:
275
225
  _expose_errors_as_service_log(
276
226
  ocm_client,
277
- cluster.ocm_cluster.external_id,
227
+ cluster.external_id,
278
228
  f"DTP can't patch Syncset {SYNCSET_ID} due to {str(e.args)}",
279
229
  )
280
230
  logging.info(f"Syncset {SYNCSET_ID} patched.")
281
231
 
282
- def get_syncset(
283
- self, ocm_client: OCMBaseClient, cluster: ClusterDetails
284
- ) -> dict[str, Any]:
232
+ def get_syncset(self, ocm_client: OCMClient, cluster: Cluster) -> dict[str, Any]:
285
233
  try:
286
- syncset = get_syncset(ocm_client, cluster.ocm_cluster.id, SYNCSET_ID)
234
+ syncset = ocm_client.get_syncset(cluster.id, SYNCSET_ID)
287
235
  except Exception as e:
288
236
  if "Not Found" in e.args[0]:
289
237
  syncset = None
@@ -291,7 +239,9 @@ class DynatraceTokenProviderIntegration(
291
239
  raise e
292
240
  return syncset
293
241
 
294
- def get_tokens_from_syncset(self, syncset: Mapping[str, Any]) -> dict:
242
+ def get_tokens_from_syncset(
243
+ self, syncset: Mapping[str, Any]
244
+ ) -> dict[str, DynatraceAPITokenCreated]:
295
245
  tokens: dict[str, Any] = {}
296
246
  for resource in syncset["resources"]:
297
247
  if resource["kind"] == "Secret":
@@ -301,20 +251,20 @@ class DynatraceTokenProviderIntegration(
301
251
  resource["data"]["dataIngestTokenId"]
302
252
  )
303
253
  ingest_token = self.base64_decode(resource["data"]["dataIngestToken"])
304
- tokens[DYNATRACE_INGESTION_TOKEN_NAME] = {
305
- "id": ingest_token_id,
306
- "token": ingest_token,
307
- }
308
- tokens[DYNATRACE_OPERATOR_TOKEN_NAME] = {
309
- "id": operator_token_id,
310
- "token": operator_token,
311
- }
254
+ tokens[DYNATRACE_INGESTION_TOKEN_NAME] = DynatraceAPITokenCreated(
255
+ id=ingest_token_id,
256
+ token=ingest_token,
257
+ )
258
+ tokens[DYNATRACE_OPERATOR_TOKEN_NAME] = DynatraceAPITokenCreated(
259
+ id=operator_token_id,
260
+ token=operator_token,
261
+ )
312
262
  return tokens
313
263
 
314
264
  def construct_base_syncset(
315
265
  self,
316
- ingestion_token: ApiTokenCreated,
317
- operator_token: ApiTokenCreated,
266
+ ingestion_token: DynatraceAPITokenCreated,
267
+ operator_token: DynatraceAPITokenCreated,
318
268
  dt_api_url: str,
319
269
  ) -> dict[str, Any]:
320
270
  return {
@@ -346,8 +296,8 @@ class DynatraceTokenProviderIntegration(
346
296
 
347
297
  def construct_syncset(
348
298
  self,
349
- ingestion_token: ApiTokenCreated,
350
- operator_token: ApiTokenCreated,
299
+ ingestion_token: DynatraceAPITokenCreated,
300
+ operator_token: DynatraceAPITokenCreated,
351
301
  dt_api_url: str,
352
302
  ) -> dict[str, Any]:
353
303
  syncset = self.construct_base_syncset(
@@ -359,17 +309,17 @@ class DynatraceTokenProviderIntegration(
359
309
  return syncset
360
310
 
361
311
  def create_dynatrace_ingestion_token(
362
- self, dt_client: Dynatrace, cluster_uuid: str
363
- ) -> ApiTokenCreated:
364
- return dt_client.tokens.create(
312
+ self, dt_client: DynatraceClient, cluster_uuid: str
313
+ ) -> DynatraceAPITokenCreated:
314
+ return dt_client.create_api_token(
365
315
  name=f"dtp-ingestion-token-{cluster_uuid}",
366
316
  scopes=["metrics.ingest", "logs.ingest", "events.ingest"],
367
317
  )
368
318
 
369
319
  def create_dynatrace_operator_token(
370
- self, dt_client: Dynatrace, cluster_uuid: str
371
- ) -> ApiTokenCreated:
372
- return dt_client.tokens.create(
320
+ self, dt_client: DynatraceClient, cluster_uuid: str
321
+ ) -> DynatraceAPITokenCreated:
322
+ return dt_client.create_api_token(
373
323
  name=f"dtp-operator-token-{cluster_uuid}",
374
324
  scopes=[
375
325
  "activeGateTokenManagement.create",
@@ -382,8 +332,8 @@ class DynatraceTokenProviderIntegration(
382
332
  )
383
333
 
384
334
  def create_dynatrace_tokens(
385
- self, dt_client: Dynatrace, cluster_uuid: str
386
- ) -> tuple[ApiTokenCreated, ApiTokenCreated]:
335
+ self, dt_client: DynatraceClient, cluster_uuid: str
336
+ ) -> tuple[DynatraceAPITokenCreated, DynatraceAPITokenCreated]:
387
337
  ingestion_token = self.create_dynatrace_ingestion_token(dt_client, cluster_uuid)
388
338
  operation_token = self.create_dynatrace_operator_token(dt_client, cluster_uuid)
389
339
  return (ingestion_token, operation_token)
@@ -394,10 +344,9 @@ def dtp_label_key(config_atom: str | None) -> str:
394
344
 
395
345
 
396
346
  def _expose_errors_as_service_log(
397
- ocm_api: OCMBaseClient, cluster_uuid: str, error: str
347
+ ocm_api: OCMClient, cluster_uuid: str, error: str
398
348
  ) -> None:
399
- create_service_log(
400
- ocm_api=ocm_api,
349
+ ocm_api.create_service_log(
401
350
  service_log=OCMClusterServiceLogCreateModel(
402
351
  cluster_uuid=cluster_uuid,
403
352
  severity=OCMServiceLogSeverity.Warning,
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from datetime import timedelta
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from reconcile.utils.ocm.base import (
10
+ OCMClusterServiceLogCreateModel,
11
+ )
12
+ from reconcile.utils.ocm.clusters import (
13
+ ClusterDetails,
14
+ discover_clusters_by_labels,
15
+ )
16
+ from reconcile.utils.ocm.labels import Filter
17
+ from reconcile.utils.ocm.service_log import create_service_log
18
+ from reconcile.utils.ocm.sre_capability_labels import sre_capability_label_key
19
+ from reconcile.utils.ocm.syncsets import (
20
+ create_syncset,
21
+ get_syncset,
22
+ patch_syncset,
23
+ )
24
+ from reconcile.utils.ocm_base_client import (
25
+ OCMBaseClient,
26
+ )
27
+
28
+ """
29
+ Thin abstractions of reconcile.ocm module to reduce coupling.
30
+ """
31
+
32
+
33
+ class Cluster(BaseModel):
34
+ id: str
35
+ external_id: str
36
+ organization_id: str
37
+ dt_tenant: str
38
+
39
+ @staticmethod
40
+ def from_cluster_details(cluster: ClusterDetails) -> Cluster:
41
+ dt_tenant = cluster.labels.get_label_value(
42
+ f"{sre_capability_label_key('dtp', None)}.tenant"
43
+ )
44
+ return Cluster(
45
+ id=cluster.ocm_cluster.id,
46
+ external_id=cluster.ocm_cluster.external_id,
47
+ organization_id=cluster.organization_id,
48
+ dt_tenant=dt_tenant,
49
+ )
50
+
51
+
52
+ class OCMClient:
53
+ """
54
+ Thin OOP wrapper around OCMBaseClient to avoid function mocking in tests
55
+ """
56
+
57
+ def __init__(self, ocm_client: OCMBaseClient):
58
+ self._ocm_client = ocm_client
59
+
60
+ def create_syncset(self, cluster_id: str, syncset_map: Mapping) -> None:
61
+ create_syncset(
62
+ ocm_client=self._ocm_client, cluster_id=cluster_id, syncset_map=syncset_map
63
+ )
64
+
65
+ def get_syncset(self, cluster_id: str, syncset_id: str) -> Any:
66
+ return get_syncset(
67
+ ocm_client=self._ocm_client, cluster_id=cluster_id, syncset_id=syncset_id
68
+ )
69
+
70
+ def patch_syncset(
71
+ self, cluster_id: str, syncset_id: str, syncset_map: Mapping
72
+ ) -> None:
73
+ patch_syncset(
74
+ ocm_client=self._ocm_client,
75
+ cluster_id=cluster_id,
76
+ syncset_id=syncset_id,
77
+ syncset_map=syncset_map,
78
+ )
79
+
80
+ def discover_clusters_by_labels(self, label_filter: Filter) -> list[Cluster]:
81
+ return [
82
+ Cluster.from_cluster_details(cluster)
83
+ for cluster in discover_clusters_by_labels(
84
+ ocm_api=self._ocm_client, label_filter=label_filter
85
+ )
86
+ ]
87
+
88
+ def create_service_log(
89
+ self,
90
+ service_log: OCMClusterServiceLogCreateModel,
91
+ dedup_interval: timedelta | None,
92
+ ) -> None:
93
+ create_service_log(
94
+ ocm_api=self._ocm_client,
95
+ service_log=service_log,
96
+ dedup_interval=dedup_interval,
97
+ )