qontract-reconcile 0.10.2.dev193__py3-none-any.whl → 0.10.2.dev195__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.dev193
3
+ Version: 0.10.2.dev195
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
@@ -187,9 +187,9 @@ reconcile/cna/assets/asset_factory.py,sha256=7T7X_J6xIsoGETqBRI45_EyIKEdQcnRPt_G
187
187
  reconcile/cna/assets/null.py,sha256=85mVh97atCoC0aLuX47poTZiyOthmziJeBsUw0c924w,1658
188
188
  reconcile/dynatrace_token_provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
189
189
  reconcile/dynatrace_token_provider/dependencies.py,sha256=lvkdwqHMsn_2kgj-tUIJdTUnUNxVoS6z8k4nPkGglnQ,3129
190
- reconcile/dynatrace_token_provider/integration.py,sha256=1D6gIEwKvkzE9JDOcLb9EAP6JW8OK2OLNuiHAyot_XQ,28329
190
+ reconcile/dynatrace_token_provider/integration.py,sha256=RTGy4A6U4EgE1G4rMdS8gqgw2XIfDcdYd-eF5DL9bo0,27166
191
191
  reconcile/dynatrace_token_provider/metrics.py,sha256=oP-6NTZENFdvWiS0krnmX6tq3xyOzQ8e6vS0CZWYUuw,1496
192
- reconcile/dynatrace_token_provider/model.py,sha256=L6THhpPnSIeJ5n61IHhDT_JTiSEr_uWmgJAw83RUC_w,477
192
+ reconcile/dynatrace_token_provider/model.py,sha256=VU2tZT_NAdoCovGFVj5ZoEKhWfMsC1PPPB8Iu9WMSAw,641
193
193
  reconcile/dynatrace_token_provider/ocm.py,sha256=EPknDhLXkySs8Nv8jrrl12oRoe2bRFWx_CMiHpPQhmM,3734
194
194
  reconcile/dynatrace_token_provider/validate.py,sha256=40_9QmHoB3-KBc0k_0D4QO00PpNNPS-gU9Z6cIcWga8,1920
195
195
  reconcile/endpoints_discovery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -662,7 +662,7 @@ reconcile/utils/ruamel.py,sha256=FzL4_L0FnMOUZmgThrZSMJs5MTdXwiy-E9MZWfk8bh8,397
662
662
  reconcile/utils/secret_reader.py,sha256=MaP56KZaAE35EyYbgAitdm6fUSxdzWeGFSOym9qiZkw,10206
663
663
  reconcile/utils/semver_helper.py,sha256=-WfPOMSA2v1h7hT3PwVf-Htg7wOsoKlQC1JdmDX2Ars,1268
664
664
  reconcile/utils/sharding.py,sha256=DDBHfs5TT9UgjmzewiXUjbncnrPuceAZWeOA4veGa7s,843
665
- reconcile/utils/slack_api.py,sha256=CKHjO1EyNpJsqZYEnN5uRMVF-soTCYwUma_Th4enHSo,17507
665
+ reconcile/utils/slack_api.py,sha256=IcnXmQrKXQwYEhKfXTBmOeuYKxpSG2Wvc1Fq-Nf1Xgg,17551
666
666
  reconcile/utils/slo_document_manager.py,sha256=CPgM2oH4AVzBqenakWo59R5yfwB62tnxSnSOHgir7l8,9500
667
667
  reconcile/utils/smtp_client.py,sha256=0xefB4I9E5eBB-FlxFJYjvz3Kvuqi_K3Ma_Wk0NAQKM,2779
668
668
  reconcile/utils/sqs_gateway.py,sha256=XNIf3PY4UCPNufP2Ul0UJj3fKlt5larBba-VTT-41Fg,2265
@@ -809,7 +809,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
809
809
  tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
810
810
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
811
811
  tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
812
- qontract_reconcile-0.10.2.dev193.dist-info/METADATA,sha256=RO4qwLdb35AWDUmBLVj6VloYRszUVF6RgCjA2_x4P_w,24555
813
- qontract_reconcile-0.10.2.dev193.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
814
- qontract_reconcile-0.10.2.dev193.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
815
- qontract_reconcile-0.10.2.dev193.dist-info/RECORD,,
812
+ qontract_reconcile-0.10.2.dev195.dist-info/METADATA,sha256=MHzXq3sy6lXSH9U2kaPoAkQ9w9LDLvg6vK7zNg4CbwQ,24555
813
+ qontract_reconcile-0.10.2.dev195.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
814
+ qontract_reconcile-0.10.2.dev195.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
815
+ qontract_reconcile-0.10.2.dev195.dist-info/RECORD,,
@@ -2,7 +2,7 @@ import base64
2
2
  import hashlib
3
3
  import logging
4
4
  from collections import Counter, defaultdict
5
- from collections.abc import Iterable, Mapping, MutableMapping
5
+ from collections.abc import Iterable, Mapping
6
6
  from datetime import timedelta
7
7
  from threading import Lock
8
8
  from typing import Any
@@ -57,6 +57,9 @@ SYNCSET_AND_MANIFEST_ID = "ext-dynatrace-tokens-dtp"
57
57
  DTP_LABEL_SEARCH = sre_capability_label_key("dtp", "%")
58
58
  DTP_TENANT_V2_LABEL = sre_capability_label_key("dtp.v2", "tenant")
59
59
  DTP_SPEC_V2_LABEL = sre_capability_label_key("dtp.v2", "token-spec")
60
+ DTP_V3_PREFIX = sre_capability_label_key("dtp", "v3")
61
+ DTP_V3_SPEC_SUFFIX = "token-spec"
62
+ DTP_V3_TENANT_SUFFIX = "tenant"
60
63
 
61
64
 
62
65
  class ReconcileErrorSummary(Exception):
@@ -110,47 +113,106 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
110
113
  cnt,
111
114
  )
112
115
 
113
- def _parse_ocm_data_to_cluster(self, ocm_cluster: OCMCluster) -> Cluster | None:
114
- dt_tenant = ocm_cluster.labels.get(DTP_TENANT_V2_LABEL)
115
- token_spec_name = ocm_cluster.labels.get(DTP_SPEC_V2_LABEL)
116
- if not dt_tenant or not token_spec_name:
117
- logging.warning(
118
- f"[Missing DTP labels] {ocm_cluster.id=} {ocm_cluster.subscription_id=} {dt_tenant=} {token_spec_name=}"
116
+ def _parse_ocm_data_to_cluster(
117
+ self, ocm_cluster: OCMCluster, dependencies: Dependencies
118
+ ) -> Cluster | None:
119
+ bindings: dict[str, TokenSpecTenantBinding] = {}
120
+ for label in ocm_cluster.labels:
121
+ if not label.startswith(DTP_V3_PREFIX):
122
+ continue
123
+ if not (
124
+ label.endswith(DTP_V3_TENANT_SUFFIX)
125
+ or label.endswith(DTP_V3_SPEC_SUFFIX)
126
+ ):
127
+ logging.warning(
128
+ f"[Bad DTPv3 label key] {label=} {ocm_cluster.id=} {ocm_cluster.subscription_id=}"
129
+ )
130
+ continue
131
+ common_prefix = label.rsplit(".", 1)[0]
132
+ if not (
133
+ tenant := ocm_cluster.labels.get(
134
+ f"{common_prefix}.{DTP_V3_TENANT_SUFFIX}"
135
+ )
136
+ ):
137
+ logging.warning(
138
+ f"[Missing {DTP_V3_TENANT_SUFFIX} for common label prefix {common_prefix=}] {ocm_cluster.id=} {ocm_cluster.subscription_id=}"
139
+ )
140
+ continue
141
+ if not (
142
+ spec_name := ocm_cluster.labels.get(
143
+ f"{common_prefix}.{DTP_V3_SPEC_SUFFIX}"
144
+ )
145
+ ):
146
+ logging.warning(
147
+ f"[Missing {DTP_V3_SPEC_SUFFIX} for common label prefix {common_prefix=}] {ocm_cluster.id=} {ocm_cluster.subscription_id=}"
148
+ )
149
+ continue
150
+ if not (spec := dependencies.token_spec_by_name.get(spec_name)):
151
+ logging.warning(
152
+ f"[Missing spec '{spec_name}'] {ocm_cluster.id=} {ocm_cluster.subscription_id=}"
153
+ )
154
+ continue
155
+ bindings[common_prefix] = TokenSpecTenantBinding(
156
+ spec=spec,
157
+ tenant_id=tenant,
119
158
  )
120
- return None
159
+
160
+ if not bindings:
161
+ # Stay backwards compatible with v2 for now
162
+ dt_tenant = ocm_cluster.labels.get(DTP_TENANT_V2_LABEL)
163
+ token_spec_name = ocm_cluster.labels.get(DTP_SPEC_V2_LABEL)
164
+ token_spec = dependencies.token_spec_by_name.get(token_spec_name or "")
165
+ if not dt_tenant or not token_spec:
166
+ logging.warning(
167
+ f"[Missing DTP labels] {ocm_cluster.id=} {ocm_cluster.subscription_id=} {dt_tenant=} {token_spec_name=}"
168
+ )
169
+ return None
170
+ bindings["v2"] = TokenSpecTenantBinding(
171
+ spec=token_spec,
172
+ tenant_id=dt_tenant,
173
+ )
174
+
175
+ bindings_list = list(bindings.values())
176
+
177
+ for binding in bindings_list:
178
+ if binding.tenant_id not in dependencies.dynatrace_client_by_tenant_id:
179
+ logging.warning(
180
+ f"[{ocm_cluster.id=}] Dynatrace {binding.tenant_id=} does not exist"
181
+ )
182
+ return None
183
+
121
184
  return Cluster(
122
185
  id=ocm_cluster.id,
123
186
  external_id=ocm_cluster.external_id,
124
187
  organization_id=ocm_cluster.organization_id,
125
188
  is_hcp=ocm_cluster.is_hcp,
126
- dt_token_bindings=[
127
- TokenSpecTenantBinding(
128
- spec_name=token_spec_name,
129
- tenant_id=dt_tenant,
130
- )
131
- ],
189
+ dt_token_bindings=bindings_list,
132
190
  )
133
191
 
134
192
  def _filter_clusters(
135
193
  self,
136
194
  clusters: Iterable[Cluster],
137
- token_spec_by_name: Mapping[str, DynatraceTokenProviderTokenSpecV1],
138
195
  ) -> list[Cluster]:
139
196
  filtered_clusters = []
140
197
  for cluster in clusters:
198
+ # Check if any token binding is valid for this cluster
199
+ has_valid_binding = False
141
200
  for token_binding in cluster.dt_token_bindings:
142
- token_spec = token_spec_by_name.get(token_binding.spec_name)
143
- if not token_spec:
144
- logging.debug(
145
- f"[{cluster.id=}] Skipping cluster. {token_binding.spec_name=} does not exist."
146
- )
147
- continue
201
+ token_spec = token_binding.spec
148
202
  if cluster.organization_id in token_spec.ocm_org_ids:
149
- filtered_clusters.append(cluster)
203
+ has_valid_binding = True
204
+ break
150
205
  else:
151
206
  logging.debug(
152
- f"[{cluster.id=}] Skipping cluster for {token_spec.name=}. {cluster.organization_id=} is not defined in {token_spec.ocm_org_ids=}."
207
+ f"[{cluster.id=}] Skipping token binding for {token_spec.name=}. {cluster.organization_id=} is not defined in {token_spec.ocm_org_ids=}."
153
208
  )
209
+
210
+ if has_valid_binding:
211
+ filtered_clusters.append(cluster)
212
+ else:
213
+ logging.debug(
214
+ f"[{cluster.id=}] Skipping cluster as it has no valid token bindings."
215
+ )
154
216
  return filtered_clusters
155
217
 
156
218
  def reconcile(self, dry_run: bool, dependencies: Dependencies) -> None:
@@ -176,13 +238,13 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
176
238
  for ocm_cluster in ocm_clusters
177
239
  if (
178
240
  cluster := self._parse_ocm_data_to_cluster(
179
- ocm_cluster=ocm_cluster
241
+ ocm_cluster=ocm_cluster,
242
+ dependencies=dependencies,
180
243
  )
181
244
  )
182
245
  ]
183
246
  filtered_clusters = self._filter_clusters(
184
247
  clusters=clusters,
185
- token_spec_by_name=dependencies.token_spec_by_name,
186
248
  )
187
249
 
188
250
  existing_dtp_tokens: dict[str, dict[str, str]] = {}
@@ -195,76 +257,21 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
195
257
  len(clusters),
196
258
  )
197
259
  for cluster in filtered_clusters:
198
- for token_binding in cluster.dt_token_bindings:
260
+ with DTPOrganizationErrorRate(
261
+ integration=self.name,
262
+ ocm_env=ocm_env_name,
263
+ org_id=cluster.organization_id,
264
+ ):
199
265
  try:
200
- with DTPOrganizationErrorRate(
201
- integration=self.name,
202
- ocm_env=ocm_env_name,
203
- org_id=cluster.organization_id,
204
- ):
205
- tenant_id = token_binding.tenant_id
206
- if not tenant_id:
207
- _expose_errors_as_service_log(
208
- ocm_client,
209
- cluster_uuid=cluster.external_id,
210
- error=f"Missing label {DTP_TENANT_V2_LABEL}",
211
- )
212
- logging.warning(
213
- f"[{cluster.id=}] Missing value for label {DTP_TENANT_V2_LABEL}"
214
- )
215
- continue
216
- if (
217
- tenant_id
218
- not in dependencies.dynatrace_client_by_tenant_id
219
- ):
220
- _expose_errors_as_service_log(
221
- ocm_client,
222
- cluster_uuid=cluster.external_id,
223
- error=f"Dynatrace tenant {tenant_id} does not exist",
224
- )
225
- logging.warning(
226
- f"[{cluster.id=}] Dynatrace {tenant_id=} does not exist"
227
- )
228
- continue
229
- dt_client = dependencies.dynatrace_client_by_tenant_id[
230
- tenant_id
231
- ]
232
-
233
- token_spec = dependencies.token_spec_by_name.get(
234
- token_binding.spec_name
235
- )
236
- if not token_spec:
237
- _expose_errors_as_service_log(
238
- ocm_client,
239
- cluster_uuid=cluster.external_id,
240
- error=f"Token spec {token_binding.spec_name} does not exist",
241
- )
242
- logging.warning(
243
- f"[{cluster.id=}] Token spec '{token_binding.spec_name}' does not exist"
244
- )
245
- continue
246
- if tenant_id not in existing_dtp_tokens:
247
- existing_dtp_tokens[tenant_id] = (
248
- dt_client.get_token_ids_map_for_name_prefix(
249
- prefix="dtp"
250
- )
251
- )
252
-
253
- """
254
- Note, that we consciously do not parallelize cluster processing
255
- for now. We want to keep stress on OCM at a minimum. The amount
256
- of tagged clusters is currently feasible to be processed sequentially.
257
- """
258
- self.process_cluster(
259
- dry_run=dry_run,
260
- cluster=cluster,
261
- dt_client=dt_client,
262
- ocm_client=ocm_client,
263
- existing_dtp_tokens=existing_dtp_tokens[tenant_id],
264
- tenant_id=tenant_id,
265
- token_spec=token_spec,
266
- ocm_env_name=ocm_env_name,
267
- )
266
+ self.process_cluster(
267
+ dry_run=dry_run,
268
+ cluster=cluster,
269
+ ocm_client=ocm_client,
270
+ existing_dtp_tokens=existing_dtp_tokens,
271
+ ocm_env_name=ocm_env_name,
272
+ dependencies=dependencies,
273
+ )
274
+
268
275
  except Exception as e:
269
276
  unhandled_exceptions.append(
270
277
  f"{ocm_env_name}/{cluster.organization_id}/{cluster.id}: {e}"
@@ -278,34 +285,57 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
278
285
  self,
279
286
  dry_run: bool,
280
287
  cluster: Cluster,
281
- dt_client: DynatraceClient,
282
288
  ocm_client: OCMClient,
283
- existing_dtp_tokens: MutableMapping[str, str],
284
- tenant_id: str,
285
- token_spec: DynatraceTokenProviderTokenSpecV1,
289
+ existing_dtp_tokens: dict[str, dict[str, str]],
286
290
  ocm_env_name: str,
291
+ dependencies: Dependencies,
287
292
  ) -> None:
288
- existing_data = {}
293
+ current_secrets: list[K8sSecret] = []
289
294
  if cluster.is_hcp:
290
- existing_data = self.get_manifest(ocm_client=ocm_client, cluster=cluster)
295
+ data = self.get_manifest(ocm_client=ocm_client, cluster=cluster)
296
+ for binding in cluster.dt_token_bindings:
297
+ current_secrets.extend(
298
+ self.get_secrets_from_manifest(
299
+ manifest=data, token_spec=binding.spec
300
+ )
301
+ )
291
302
  else:
292
- existing_data = self.get_syncset(ocm_client=ocm_client, cluster=cluster)
293
- dt_api_url = f"https://{tenant_id}.live.dynatrace.com/api"
294
- if not existing_data:
303
+ data = self.get_syncset(ocm_client=ocm_client, cluster=cluster)
304
+ for binding in cluster.dt_token_bindings:
305
+ current_secrets.extend(
306
+ self.get_secrets_from_syncset(syncset=data, token_spec=binding.spec)
307
+ )
308
+
309
+ desired_secrets: list[K8sSecret] = []
310
+ has_diff = False
311
+ for binding in cluster.dt_token_bindings:
312
+ dt_client = dependencies.dynatrace_client_by_tenant_id[binding.tenant_id]
313
+ if binding.tenant_id not in existing_dtp_tokens:
314
+ existing_dtp_tokens[binding.tenant_id] = (
315
+ dt_client.get_token_ids_map_for_name_prefix(prefix="dtp")
316
+ )
317
+ cur_diff, cur_desired_secrets = self.generate_desired(
318
+ dry_run=dry_run,
319
+ current_k8s_secrets=current_secrets,
320
+ desired_spec=binding.spec,
321
+ existing_dtp_tokens=existing_dtp_tokens[binding.tenant_id],
322
+ dt_client=dt_client,
323
+ cluster_uuid=cluster.external_id,
324
+ dt_tenant_id=binding.tenant_id,
325
+ ocm_env_name=ocm_env_name,
326
+ )
327
+ desired_secrets.extend(cur_desired_secrets)
328
+ has_diff |= cur_diff
329
+
330
+ if not current_secrets:
295
331
  if not dry_run:
296
332
  try:
297
- k8s_secrets = self.construct_secrets(
298
- token_spec=token_spec,
299
- dt_client=dt_client,
300
- cluster_uuid=cluster.external_id,
301
- )
302
333
  if cluster.is_hcp:
303
334
  ocm_client.create_manifest(
304
335
  cluster_id=cluster.id,
305
336
  manifest_map=self.construct_manifest(
306
337
  with_id=True,
307
- dt_api_url=dt_api_url,
308
- secrets=k8s_secrets,
338
+ secrets=desired_secrets,
309
339
  ),
310
340
  )
311
341
  else:
@@ -313,72 +343,47 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
313
343
  cluster_id=cluster.id,
314
344
  syncset_map=self.construct_syncset(
315
345
  with_id=True,
316
- dt_api_url=dt_api_url,
317
- secrets=k8s_secrets,
346
+ secrets=desired_secrets,
318
347
  ),
319
348
  )
320
349
  except Exception as e:
321
350
  _expose_errors_as_service_log(
322
351
  ocm_client,
323
352
  cluster.external_id,
324
- f"DTP can't create {token_spec.name=} {e.args!s}",
353
+ f"DTP can't create {SYNCSET_AND_MANIFEST_ID} due to {e.args!s}",
325
354
  )
326
- logging.info(
327
- f"{token_spec.name=} created in {dt_api_url} for {cluster.id=}."
328
- )
329
355
  logging.info(f"{SYNCSET_AND_MANIFEST_ID} created for {cluster.id=}.")
330
- else:
331
- current_k8s_secrets: list[K8sSecret] = []
332
- if cluster.is_hcp:
333
- current_k8s_secrets = self.get_secrets_from_manifest(
334
- manifest=existing_data, token_spec=token_spec
335
- )
336
- else:
337
- current_k8s_secrets = self.get_secrets_from_syncset(
338
- syncset=existing_data, token_spec=token_spec
339
- )
340
- has_diff, desired_secrets = self.generate_desired(
341
- dry_run=dry_run,
342
- current_k8s_secrets=current_k8s_secrets,
343
- desired_spec=token_spec,
344
- existing_dtp_tokens=existing_dtp_tokens,
345
- dt_client=dt_client,
346
- cluster_uuid=cluster.external_id,
347
- dt_tenant_id=tenant_id,
348
- ocm_env_name=ocm_env_name,
349
- )
350
- if has_diff:
351
- if not dry_run:
352
- try:
353
- if cluster.is_hcp:
354
- ocm_client.patch_manifest(
355
- cluster_id=cluster.id,
356
- manifest_id=SYNCSET_AND_MANIFEST_ID,
357
- manifest_map=self.construct_manifest(
358
- dt_api_url=dt_api_url,
359
- secrets=desired_secrets,
360
- with_id=False,
361
- ),
362
- )
363
- else:
364
- ocm_client.patch_syncset(
365
- cluster_id=cluster.id,
366
- syncset_id=SYNCSET_AND_MANIFEST_ID,
367
- syncset_map=self.construct_syncset(
368
- dt_api_url=dt_api_url,
369
- secrets=desired_secrets,
370
- with_id=False,
371
- ),
372
- )
373
- except Exception as e:
374
- _expose_errors_as_service_log(
375
- ocm_client,
376
- cluster.external_id,
377
- f"DTP can't patch {token_spec.name=} for {SYNCSET_AND_MANIFEST_ID} due to {e.args!s}",
356
+ elif has_diff:
357
+ if not dry_run:
358
+ try:
359
+ if cluster.is_hcp:
360
+ ocm_client.patch_manifest(
361
+ cluster_id=cluster.id,
362
+ manifest_id=SYNCSET_AND_MANIFEST_ID,
363
+ manifest_map=self.construct_manifest(
364
+ secrets=desired_secrets,
365
+ with_id=False,
366
+ ),
378
367
  )
379
- logging.info(
380
- f"Patched {token_spec.name=} for {SYNCSET_AND_MANIFEST_ID} in {cluster.id=}."
381
- )
368
+ else:
369
+ ocm_client.patch_syncset(
370
+ cluster_id=cluster.id,
371
+ syncset_id=SYNCSET_AND_MANIFEST_ID,
372
+ syncset_map=self.construct_syncset(
373
+ secrets=desired_secrets,
374
+ with_id=False,
375
+ ),
376
+ )
377
+ except Exception as e:
378
+ _expose_errors_as_service_log(
379
+ ocm_client,
380
+ cluster.external_id,
381
+ f"DTP can't patch {SYNCSET_AND_MANIFEST_ID} due to {e.args!s}",
382
+ )
383
+ logging.info(f"Patched {SYNCSET_AND_MANIFEST_ID} in {cluster.id=}.")
384
+
385
+ def dt_api_url(self, tenant_id: str) -> str:
386
+ return f"https://{tenant_id}.live.dynatrace.com/api"
382
387
 
383
388
  def scopes_hash(self, scopes: Iterable[str], length: int) -> str:
384
389
  m = hashlib.sha256()
@@ -428,7 +433,7 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
428
433
  dry_run: bool,
429
434
  current_k8s_secrets: Iterable[K8sSecret],
430
435
  desired_spec: DynatraceTokenProviderTokenSpecV1,
431
- existing_dtp_tokens: MutableMapping[str, str],
436
+ existing_dtp_tokens: dict[str, str],
432
437
  dt_client: DynatraceClient,
433
438
  cluster_uuid: str,
434
439
  ocm_env_name: str,
@@ -475,6 +480,7 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
475
480
  secret_name=secret.name,
476
481
  namespace_name=secret.namespace,
477
482
  tokens=desired_tokens,
483
+ dt_api_url=self.dt_api_url(tenant_id=dt_tenant_id),
478
484
  )
479
485
  )
480
486
 
@@ -500,6 +506,7 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
500
506
  self,
501
507
  token_spec: DynatraceTokenProviderTokenSpecV1,
502
508
  dt_client: DynatraceClient,
509
+ dt_api_url: str,
503
510
  cluster_uuid: str,
504
511
  ) -> list[K8sSecret]:
505
512
  secrets: list[K8sSecret] = []
@@ -513,6 +520,7 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
513
520
  secret_name=secret.name,
514
521
  namespace_name=secret.namespace,
515
522
  tokens=new_tokens,
523
+ dt_api_url=dt_api_url,
516
524
  )
517
525
  )
518
526
  return secrets
@@ -562,11 +570,13 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
562
570
  secret_key=token.key_name_in_secret,
563
571
  )
564
572
  )
573
+ dt_api_url = self.base64_decode(secret_data.get("apiUrl", ""))
565
574
  secrets.append(
566
575
  K8sSecret(
567
576
  secret_name=secret.name,
568
577
  namespace_name=secret.namespace,
569
578
  tokens=tokens,
579
+ dt_api_url=dt_api_url,
570
580
  )
571
581
  )
572
582
  return secrets
@@ -574,6 +584,8 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
574
584
  def get_secrets_from_syncset(
575
585
  self, syncset: Mapping[str, Any], token_spec: DynatraceTokenProviderTokenSpecV1
576
586
  ) -> list[K8sSecret]:
587
+ if not syncset:
588
+ return []
577
589
  secret_data_by_name = {
578
590
  resource.get("metadata", {}).get("name"): resource.get("data", {})
579
591
  for resource in syncset.get("resources", [])
@@ -586,6 +598,8 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
586
598
  def get_secrets_from_manifest(
587
599
  self, manifest: Mapping[str, Any], token_spec: DynatraceTokenProviderTokenSpecV1
588
600
  ) -> list[K8sSecret]:
601
+ if not manifest:
602
+ return []
589
603
  secret_data_by_name = {
590
604
  resource.get("metadata", {}).get("name"): resource.get("data", {})
591
605
  for resource in manifest.get("workloads", [])
@@ -598,12 +612,11 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
598
612
  def construct_secrets_data(
599
613
  self,
600
614
  secrets: Iterable[K8sSecret],
601
- dt_api_url: str,
602
615
  ) -> list[dict[str, Any]]:
603
616
  secrets_data: list[dict[str, Any]] = []
604
617
  for secret in secrets:
605
618
  data: dict[str, str] = {
606
- "apiUrl": f"{self.base64_encode_str(dt_api_url)}",
619
+ "apiUrl": f"{self.base64_encode_str(secret.dt_api_url)}",
607
620
  }
608
621
  for token in secret.tokens:
609
622
  data[token.secret_key] = f"{self.base64_encode_str(token.token)}"
@@ -626,25 +639,19 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
626
639
  def construct_base_syncset(
627
640
  self,
628
641
  secrets: Iterable[K8sSecret],
629
- dt_api_url: str,
630
642
  ) -> dict[str, Any]:
631
643
  return {
632
644
  "kind": "SyncSet",
633
- "resources": self.construct_secrets_data(
634
- secrets=secrets, dt_api_url=dt_api_url
635
- ),
645
+ "resources": self.construct_secrets_data(secrets=secrets),
636
646
  }
637
647
 
638
648
  def construct_base_manifest(
639
649
  self,
640
650
  secrets: Iterable[K8sSecret],
641
- dt_api_url: str,
642
651
  ) -> dict[str, Any]:
643
652
  return {
644
653
  "kind": "Manifest",
645
- "workloads": self.construct_secrets_data(
646
- secrets=secrets, dt_api_url=dt_api_url
647
- ),
654
+ "workloads": self.construct_secrets_data(secrets=secrets),
648
655
  }
649
656
 
650
657
  def base64_decode(self, encoded: str) -> str:
@@ -659,12 +666,10 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
659
666
  def construct_syncset(
660
667
  self,
661
668
  secrets: Iterable[K8sSecret],
662
- dt_api_url: str,
663
669
  with_id: bool,
664
670
  ) -> dict[str, Any]:
665
671
  syncset = self.construct_base_syncset(
666
672
  secrets=secrets,
667
- dt_api_url=dt_api_url,
668
673
  )
669
674
  if with_id:
670
675
  syncset["id"] = SYNCSET_AND_MANIFEST_ID
@@ -673,12 +678,10 @@ class DynatraceTokenProviderIntegration(QontractReconcileIntegration[NoParams]):
673
678
  def construct_manifest(
674
679
  self,
675
680
  secrets: Iterable[K8sSecret],
676
- dt_api_url: str,
677
681
  with_id: bool,
678
682
  ) -> dict[str, Any]:
679
683
  manifest = self.construct_base_manifest(
680
684
  secrets=secrets,
681
- dt_api_url=dt_api_url,
682
685
  )
683
686
  if with_id:
684
687
  manifest["id"] = SYNCSET_AND_MANIFEST_ID
@@ -1,5 +1,9 @@
1
1
  from pydantic import BaseModel
2
2
 
3
+ from reconcile.gql_definitions.dynatrace_token_provider.token_specs import (
4
+ DynatraceTokenProviderTokenSpecV1,
5
+ )
6
+
3
7
 
4
8
  class DynatraceAPIToken(BaseModel):
5
9
  token: str
@@ -11,11 +15,12 @@ class DynatraceAPIToken(BaseModel):
11
15
  class K8sSecret(BaseModel):
12
16
  namespace_name: str
13
17
  secret_name: str
18
+ dt_api_url: str
14
19
  tokens: list[DynatraceAPIToken]
15
20
 
16
21
 
17
22
  class TokenSpecTenantBinding(BaseModel):
18
- spec_name: str
23
+ spec: DynatraceTokenProviderTokenSpecV1
19
24
  tenant_id: str
20
25
 
21
26
 
@@ -463,7 +463,7 @@ class SlackApi:
463
463
  for r in result[result_key]:
464
464
  results[r["id"]] = r
465
465
 
466
- cursor = result["response_metadata"]["next_cursor"]
466
+ cursor = (result.get("response_metadata") or {}).get("next_cursor") or ""
467
467
 
468
468
  if not cursor:
469
469
  break
@@ -517,7 +517,7 @@ class SlackApi:
517
517
  if not keep_fetching:
518
518
  break
519
519
 
520
- cursor = response["response_metadata"]["next_cursor"]
520
+ cursor = (response.get("response_metadata") or {}).get("next_cursor") or ""
521
521
  if not cursor:
522
522
  break
523
523