qontract-reconcile 0.10.2.dev345__py3-none-any.whl → 0.10.2.dev408__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.
Files changed (126) hide show
  1. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/METADATA +11 -10
  2. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/RECORD +126 -120
  3. reconcile/aus/base.py +17 -14
  4. reconcile/automated_actions/config/integration.py +12 -0
  5. reconcile/aws_account_manager/integration.py +2 -2
  6. reconcile/aws_ami_cleanup/integration.py +6 -7
  7. reconcile/aws_ami_share.py +69 -62
  8. reconcile/aws_cloudwatch_log_retention/integration.py +155 -126
  9. reconcile/aws_ecr_image_pull_secrets.py +2 -2
  10. reconcile/aws_iam_keys.py +1 -0
  11. reconcile/aws_saml_idp/integration.py +7 -1
  12. reconcile/aws_saml_roles/integration.py +9 -3
  13. reconcile/change_owners/change_owners.py +1 -1
  14. reconcile/change_owners/diff.py +2 -4
  15. reconcile/checkpoint.py +11 -3
  16. reconcile/cli.py +33 -8
  17. reconcile/dashdotdb_dora.py +4 -11
  18. reconcile/database_access_manager.py +118 -111
  19. reconcile/endpoints_discovery/integration.py +4 -1
  20. reconcile/endpoints_discovery/merge_request_manager.py +9 -11
  21. reconcile/external_resources/factories.py +5 -12
  22. reconcile/external_resources/integration.py +1 -1
  23. reconcile/external_resources/manager.py +5 -3
  24. reconcile/external_resources/meta.py +0 -1
  25. reconcile/external_resources/model.py +10 -10
  26. reconcile/external_resources/reconciler.py +5 -2
  27. reconcile/external_resources/secrets_sync.py +4 -6
  28. reconcile/external_resources/state.py +5 -4
  29. reconcile/gabi_authorized_users.py +8 -5
  30. reconcile/gitlab_housekeeping.py +13 -15
  31. reconcile/gitlab_mr_sqs_consumer.py +2 -2
  32. reconcile/gitlab_owners.py +15 -11
  33. reconcile/gql_definitions/automated_actions/instance.py +41 -2
  34. reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +10 -0
  35. reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +22 -61
  36. reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +10 -0
  37. reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +10 -0
  38. reconcile/gql_definitions/common/aws_vpc_requests.py +10 -0
  39. reconcile/gql_definitions/common/clusters.py +2 -0
  40. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +84 -1
  41. reconcile/gql_definitions/external_resources/external_resources_settings.py +2 -0
  42. reconcile/gql_definitions/fragments/aws_account_common.py +2 -0
  43. reconcile/gql_definitions/fragments/aws_organization.py +33 -0
  44. reconcile/gql_definitions/fragments/aws_vpc_request.py +2 -0
  45. reconcile/gql_definitions/introspection.json +3474 -1986
  46. reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +4 -0
  47. reconcile/gql_definitions/terraform_init/aws_accounts.py +14 -0
  48. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +33 -1
  49. reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +10 -0
  50. reconcile/jenkins_worker_fleets.py +1 -0
  51. reconcile/jira_permissions_validator.py +236 -121
  52. reconcile/ocm/types.py +6 -0
  53. reconcile/openshift_base.py +47 -1
  54. reconcile/openshift_cluster_bots.py +2 -1
  55. reconcile/openshift_resources_base.py +6 -2
  56. reconcile/openshift_saas_deploy.py +2 -2
  57. reconcile/openshift_saas_deploy_trigger_cleaner.py +3 -5
  58. reconcile/openshift_upgrade_watcher.py +3 -3
  59. reconcile/queries.py +131 -0
  60. reconcile/saas_auto_promotions_manager/subscriber.py +4 -3
  61. reconcile/slack_usergroups.py +4 -3
  62. reconcile/sql_query.py +1 -0
  63. reconcile/statuspage/integrations/maintenances.py +4 -3
  64. reconcile/statuspage/status.py +5 -8
  65. reconcile/templates/rosa-classic-cluster-creation.sh.j2 +4 -0
  66. reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +3 -0
  67. reconcile/templating/renderer.py +2 -1
  68. reconcile/terraform_aws_route53.py +7 -1
  69. reconcile/terraform_init/integration.py +185 -21
  70. reconcile/terraform_resources.py +11 -1
  71. reconcile/terraform_tgw_attachments.py +7 -1
  72. reconcile/terraform_users.py +7 -0
  73. reconcile/terraform_vpc_peerings.py +14 -3
  74. reconcile/terraform_vpc_resources/integration.py +7 -0
  75. reconcile/typed_queries/aws_account_tags.py +41 -0
  76. reconcile/typed_queries/saas_files.py +2 -2
  77. reconcile/utils/aggregated_list.py +4 -3
  78. reconcile/utils/aws_api.py +51 -20
  79. reconcile/utils/aws_api_typed/api.py +38 -9
  80. reconcile/utils/aws_api_typed/cloudformation.py +149 -0
  81. reconcile/utils/aws_api_typed/logs.py +73 -0
  82. reconcile/utils/datetime_util.py +67 -0
  83. reconcile/utils/differ.py +2 -3
  84. reconcile/utils/early_exit_cache.py +3 -2
  85. reconcile/utils/expiration.py +7 -3
  86. reconcile/utils/external_resource_spec.py +24 -1
  87. reconcile/utils/filtering.py +1 -1
  88. reconcile/utils/helm.py +2 -1
  89. reconcile/utils/helpers.py +1 -1
  90. reconcile/utils/jinja2/utils.py +4 -96
  91. reconcile/utils/jira_client.py +82 -63
  92. reconcile/utils/jjb_client.py +9 -12
  93. reconcile/utils/jobcontroller/controller.py +1 -1
  94. reconcile/utils/jobcontroller/models.py +17 -1
  95. reconcile/utils/json.py +32 -0
  96. reconcile/utils/merge_request_manager/merge_request_manager.py +3 -3
  97. reconcile/utils/merge_request_manager/parser.py +2 -2
  98. reconcile/utils/mr/app_interface_reporter.py +2 -2
  99. reconcile/utils/mr/base.py +2 -2
  100. reconcile/utils/mr/notificator.py +2 -2
  101. reconcile/utils/mr/update_access_report_base.py +3 -4
  102. reconcile/utils/oc.py +113 -95
  103. reconcile/utils/oc_filters.py +3 -3
  104. reconcile/utils/ocm/products.py +6 -0
  105. reconcile/utils/ocm/search_filters.py +3 -6
  106. reconcile/utils/ocm/service_log.py +3 -5
  107. reconcile/utils/openshift_resource.py +10 -5
  108. reconcile/utils/output.py +3 -2
  109. reconcile/utils/pagerduty_api.py +5 -5
  110. reconcile/utils/runtime/integration.py +1 -2
  111. reconcile/utils/runtime/runner.py +2 -2
  112. reconcile/utils/saasherder/models.py +2 -1
  113. reconcile/utils/saasherder/saasherder.py +9 -7
  114. reconcile/utils/slack_api.py +24 -2
  115. reconcile/utils/sloth.py +171 -2
  116. reconcile/utils/sqs_gateway.py +2 -1
  117. reconcile/utils/state.py +2 -1
  118. reconcile/utils/terraform_client.py +4 -3
  119. reconcile/utils/terrascript_aws_client.py +165 -111
  120. reconcile/utils/vault.py +1 -1
  121. reconcile/vault_replication.py +107 -42
  122. tools/app_interface_reporter.py +4 -4
  123. tools/cli_commands/systems_and_tools.py +5 -1
  124. tools/qontract_cli.py +25 -13
  125. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/WHEEL +0 -0
  126. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/entry_points.txt +0 -0
reconcile/utils/oc.py CHANGED
@@ -12,7 +12,6 @@ import threading
12
12
  import time
13
13
  from contextlib import suppress
14
14
  from dataclasses import dataclass
15
- from datetime import datetime
16
15
  from functools import cache, wraps
17
16
  from subprocess import Popen
18
17
  from threading import Lock
@@ -47,6 +46,8 @@ from sretoolbox.utils import (
47
46
  )
48
47
 
49
48
  from reconcile.status import RunningState
49
+ from reconcile.utils.datetime_util import utc_now
50
+ from reconcile.utils.json import json_dumps
50
51
  from reconcile.utils.jump_host import (
51
52
  JumphostParameters,
52
53
  JumpHostSSH,
@@ -67,7 +68,8 @@ if TYPE_CHECKING:
67
68
  urllib3.disable_warnings()
68
69
 
69
70
  GET_REPLICASET_MAX_ATTEMPTS = 20
70
-
71
+ DEFAULT_GROUP = ""
72
+ PROJECT_KIND = "Project.project.openshift.io"
71
73
 
72
74
  oc_run_execution_counter = Counter(
73
75
  name="oc_run_execution_counter",
@@ -144,6 +146,14 @@ class RequestEntityTooLargeError(Exception):
144
146
  pass
145
147
 
146
148
 
149
+ class KindNotFoundError(Exception):
150
+ pass
151
+
152
+
153
+ class AmbiguousResourceTypeError(Exception):
154
+ pass
155
+
156
+
147
157
  class OCDecorators:
148
158
  @classmethod
149
159
  def process_reconcile_time(cls, function: Callable) -> Callable:
@@ -379,10 +389,7 @@ class OCCli:
379
389
 
380
390
  self.init_projects = init_projects
381
391
  if self.init_projects:
382
- if self.is_kind_supported("Project"):
383
- kind = "Project.project.openshift.io"
384
- else:
385
- kind = "Namespace"
392
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
386
393
  self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
387
394
 
388
395
  self.slow_oc_reconcile_threshold = float(
@@ -452,10 +459,7 @@ class OCCli:
452
459
 
453
460
  self.init_projects = init_projects
454
461
  if self.init_projects:
455
- if self.is_kind_supported("Project"):
456
- kind = "Project.project.openshift.io"
457
- else:
458
- kind = "Namespace"
462
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
459
463
  self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
460
464
 
461
465
  self.slow_oc_reconcile_threshold = float(
@@ -563,7 +567,7 @@ class OCCli:
563
567
  "-f",
564
568
  "-",
565
569
  ] + parameters_to_process
566
- result = self._run(cmd, stdin=json.dumps(template, sort_keys=True))
570
+ result = self._run(cmd, stdin=json_dumps(template))
567
571
  return json.loads(result)["items"]
568
572
 
569
573
  @OCDecorators.process_reconcile_time
@@ -592,7 +596,7 @@ class OCCli:
592
596
  def patch(
593
597
  self, namespace: str, kind: str, name: str, patch: Mapping[str, Any]
594
598
  ) -> OCProcessReconcileTimeDecoratorMsg:
595
- cmd = ["patch", "-n", namespace, kind, name, "-p", json.dumps(patch)]
599
+ cmd = ["patch", "-n", namespace, kind, name, "-p", json_dumps(patch)]
596
600
  self._run(cmd)
597
601
  resource = OR({"kind": kind, "metadata": {"name": name}}, "", "")
598
602
  return self._msg_to_process_reconcile_time(namespace, resource)
@@ -636,11 +640,9 @@ class OCCli:
636
640
  def project_exists(self, name: str) -> bool:
637
641
  if name in self.projects:
638
642
  return True
643
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
639
644
  try:
640
- if self.is_kind_supported("Project"):
641
- self.get(None, "Project.project.openshift.io", name)
642
- else:
643
- self.get(None, "Namespace", name)
645
+ self.get(None, kind, name)
644
646
  except StatusCodeError as e:
645
647
  if "NotFound" in str(e):
646
648
  return False
@@ -649,7 +651,7 @@ class OCCli:
649
651
 
650
652
  @OCDecorators.process_reconcile_time
651
653
  def new_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
652
- if self.is_kind_supported("Project"):
654
+ if self.is_kind_supported(PROJECT_KIND):
653
655
  cmd = ["new-project", namespace]
654
656
  else:
655
657
  cmd = ["create", "namespace", namespace]
@@ -665,7 +667,7 @@ class OCCli:
665
667
 
666
668
  @OCDecorators.process_reconcile_time
667
669
  def delete_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
668
- if self.is_kind_supported("Project"):
670
+ if self.is_kind_supported(PROJECT_KIND):
669
671
  cmd = ["delete", "project", namespace]
670
672
  else:
671
673
  cmd = ["delete", "namespace", namespace]
@@ -716,7 +718,7 @@ class OCCli:
716
718
  cmd = ["sa", "-n", namespace, "get-token", name]
717
719
  return self._run(cmd)
718
720
 
719
- def get_api_resources(self) -> dict[str, Any]:
721
+ def get_api_resources(self) -> dict[str, list[OCCliApiResource]]:
720
722
  with self.api_resources_lock:
721
723
  if not self.api_resources:
722
724
  cmd = ["api-resources", "--no-headers"]
@@ -1009,7 +1011,7 @@ class OCCli:
1009
1011
  name = obj["metadata"]["name"]
1010
1012
  logging.info([f"recycle_{kind.lower()}", self.cluster_name, namespace, name])
1011
1013
  if not dry_run:
1012
- now = datetime.now()
1014
+ now = utc_now()
1013
1015
  recycle_time = now.strftime("%d/%m/%Y %H:%M:%S")
1014
1016
 
1015
1017
  # get the object in case it was modified
@@ -1020,7 +1022,7 @@ class OCCli:
1020
1022
  a["recycle.time"] = recycle_time
1021
1023
  obj["spec"]["template"]["metadata"]["annotations"] = a
1022
1024
  cmd = ["apply", "-n", namespace, "-f", "-"]
1023
- stdin = json.dumps(obj, sort_keys=True)
1025
+ stdin = json_dumps(obj)
1024
1026
  self._run(cmd, stdin=stdin, apply=True)
1025
1027
 
1026
1028
  def get_obj_root_owner(
@@ -1195,76 +1197,90 @@ class OCCli:
1195
1197
 
1196
1198
  return out_json
1197
1199
 
1198
- def _parse_kind(self, kind_name: str) -> tuple[str, str]:
1199
- # This is a provisional solution while we work in redefining
1200
- # the api resources initialization.
1201
- if not self.api_resources:
1202
- self.get_api_resources()
1200
+ def parse_kind(self, kind: str) -> tuple[str, str, str]:
1201
+ """Parse a Kubernetes kind string into its components.
1203
1202
 
1204
- kind_group = kind_name.split(".", 1)
1205
- kind = kind_group[0]
1206
- if kind in self.api_resources:
1207
- group_version = self.api_resources[kind][0].group_version
1208
- else:
1209
- raise StatusCodeError(f"{self.server}: {kind} does not exist")
1210
-
1211
- # if a kind_group has more than 1 entry than the kind_name is in
1212
- # the format kind.apigroup. Find the apigroup/version that matches
1213
- # the apigroup passed with the kind_name
1214
- if len(kind_group) > 1:
1215
- apigroup_override = kind_group[1]
1216
- find = False
1217
- for gv in self.api_resources[kind]:
1218
- if apigroup_override == gv.group:
1219
- if not gv.group:
1220
- group_version = gv.api_version
1221
- else:
1222
- group_version = f"{gv.group}/{gv.api_version}"
1223
- find = True
1224
- break
1203
+ Supports three formats:
1204
+ - kind
1205
+ - kind.group.whatever
1206
+ - kind.group.whatever/version
1225
1207
 
1226
- if not find:
1227
- raise StatusCodeError(
1228
- f"{self.server}: {apigroup_override} does not have kind {kind}"
1229
- )
1230
- return (kind, group_version)
1208
+ Args:
1209
+ kind: A Kubernetes kind string in one of the supported formats
1210
+
1211
+ Returns:
1212
+ Tuple of (kind, group, version) where missing parts are empty strings
1213
+
1214
+ Raises:
1215
+ ValueError: If the kind string format is invalid
1216
+
1217
+ Examples:
1218
+ >>> parse_kind_string("Deployment")
1219
+ ('Deployment', '', '')
1220
+ >>> parse_kind_string("ClusterRoleBinding.rbac.authorization.k8s.io")
1221
+ ('ClusterRoleBinding', 'rbac.authorization.k8s.io', '')
1222
+ >>> parse_kind_string("CustomResource.mygroup.example.com/v1")
1223
+ ('CustomResource', 'mygroup.example.com', 'v1')
1224
+ """
1225
+ pattern = r"^(?P<kind>[^./]+)(?:\.(?P<group>[^/]+))?(?:/(?P<version>.+))?$"
1226
+ match = re.match(pattern, kind)
1227
+ if not match:
1228
+ raise ValueError(f"Invalid kind string: {kind}")
1229
+
1230
+ kind = match.group("kind") or ""
1231
+ group = match.group("group") or DEFAULT_GROUP
1232
+ version = match.group("version") or ""
1233
+
1234
+ return kind, group, version
1231
1235
 
1232
1236
  def is_kind_supported(self, kind: str) -> bool:
1233
- # This is a provisional solution while we work in redefining
1234
- # the api resources initialization.
1235
- if not self.api_resources:
1236
- self.get_api_resources()
1237
+ """Returns True if the given kind is supported by the cluster, False otherwise.
1237
1238
 
1238
- if "." in kind:
1239
- try:
1240
- self._parse_kind(kind)
1241
- return True
1242
- except StatusCodeError:
1243
- return False
1244
- else:
1245
- return kind in self.api_resources
1239
+ Kind can be either kind, kind.group or kind.group/version."""
1240
+ try:
1241
+ self.get_api_resource(kind)
1242
+ return True
1243
+ except KindNotFoundError:
1244
+ return False
1246
1245
 
1247
1246
  def is_kind_namespaced(self, kind: str) -> bool:
1248
- # This is a provisional solution while we work in redefining
1249
- # the api resources initialization.
1247
+ """Returns True if the given kind is namespaced, False if it's cluster scoped.
1248
+
1249
+ Kind can be either kind, kind.group or kind.group/version."""
1250
+ return self.get_api_resource(kind).namespaced
1251
+
1252
+ def get_api_resource(self, kind: str) -> OCCliApiResource:
1253
+ """Return the OCCliApiResource for the given resource type.
1254
+
1255
+ Resource type can be either kind, kind.group or kind.group/version.
1256
+ If kind is not unique, group must be specified."""
1257
+
1250
1258
  if not self.api_resources:
1251
- self.get_api_resources()
1259
+ raise RuntimeError("API resources not initialized")
1252
1260
 
1253
- kg = kind.split(".", 1)
1254
- kind = kg[0]
1261
+ kind, group, _ = self.parse_kind(kind)
1255
1262
 
1256
- # Same Kinds might exist in different api groups
1257
- kind_resources = self.api_resources.get(kind)
1258
- if not kind_resources:
1259
- raise StatusCodeError(f"Kind {kind} does not exist in the ApiServer")
1263
+ if not (resources := self.api_resources.get(kind)):
1264
+ # the kind not found at all
1265
+ raise KindNotFoundError(f"Unsupported resource type: {kind}")
1260
1266
 
1261
- if len(kg) > 1:
1262
- group = kg[1]
1263
- for r in kind_resources:
1264
- if group == r.group:
1265
- return r.namespaced
1266
- raise StatusCodeError(f"Kind: {kind} does nod exist in the ApiServer")
1267
- return kind_resources[0].namespaced
1267
+ if len(resources) == 1 and group == DEFAULT_GROUP:
1268
+ return resources[0]
1269
+
1270
+ # get the resource with the specified group
1271
+ if resource := next((r for r in resources if r.group == group), None):
1272
+ return resource
1273
+
1274
+ # no resource with the specified group found
1275
+ if group == DEFAULT_GROUP:
1276
+ message = (
1277
+ f"Ambiguous resource type: {kind}. "
1278
+ "Please fully qualify it with its API group. E.g., ClusterRoleBinding -> ClusterRoleBinding.rbac.authorization.k8s.io"
1279
+ )
1280
+ raise AmbiguousResourceTypeError(message)
1281
+
1282
+ # group was specified but no matching resource found
1283
+ raise KindNotFoundError(f"Unsupported resource type: {kind}")
1268
1284
 
1269
1285
 
1270
1286
  REQUEST_TIMEOUT = 60
@@ -1304,20 +1320,16 @@ class OCNative(OCCli):
1304
1320
 
1305
1321
  server = connection_parameters.server_url
1306
1322
 
1307
- if server:
1308
- self.client = self._get_client(server, token)
1309
- self.api_resources = self.get_api_resources()
1323
+ if not server:
1324
+ raise Exception("Server name is required!")
1310
1325
 
1311
- else:
1312
- raise Exception("A method relies on client/api_kind_version to be set")
1326
+ self.client = self._get_client(server, token)
1327
+ self.api_resources = self.get_api_resources()
1313
1328
 
1314
1329
  self.projects = set()
1315
1330
  self.init_projects = init_projects
1316
1331
  if self.init_projects:
1317
- if self.is_kind_supported("Project"):
1318
- kind = "Project.project.openshift.io"
1319
- else:
1320
- kind = "Namespace"
1332
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
1321
1333
  self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
1322
1334
 
1323
1335
  def __enter__(self) -> OCNative:
@@ -1367,8 +1379,10 @@ class OCNative(OCCli):
1367
1379
 
1368
1380
  @retry(max_attempts=5, exceptions=(ServerTimeoutError))
1369
1381
  def get_items(self, kind: str, **kwargs: Any) -> list[dict[str, Any]]:
1370
- k, group_version = self._parse_kind(kind)
1371
- obj_client = self._get_obj_client(group_version=group_version, kind=k)
1382
+ resource = self.get_api_resource(kind)
1383
+ obj_client = self._get_obj_client(
1384
+ group_version=resource.group_version, kind=resource.kind
1385
+ )
1372
1386
 
1373
1387
  namespace = ""
1374
1388
  if "namespace" in kwargs:
@@ -1420,8 +1434,10 @@ class OCNative(OCCli):
1420
1434
  name: str | None = None,
1421
1435
  allow_not_found: bool = False,
1422
1436
  ) -> dict[str, Any]:
1423
- k, group_version = self._parse_kind(kind)
1424
- obj_client = self._get_obj_client(group_version=group_version, kind=k)
1437
+ resource = self.get_api_resource(kind)
1438
+ obj_client = self._get_obj_client(
1439
+ group_version=resource.group_version, kind=resource.kind
1440
+ )
1425
1441
  try:
1426
1442
  obj = obj_client.get(
1427
1443
  name=name,
@@ -1435,8 +1451,10 @@ class OCNative(OCCli):
1435
1451
  raise StatusCodeError(f"[{self.server}]: {e}") from None
1436
1452
 
1437
1453
  def get_all(self, kind: str, all_namespaces: bool = False) -> dict[str, Any]:
1438
- k, group_version = self._parse_kind(kind)
1439
- obj_client = self._get_obj_client(group_version=group_version, kind=k)
1454
+ resource = self.get_api_resource(kind)
1455
+ obj_client = self._get_obj_client(
1456
+ group_version=resource.group_version, kind=resource.kind
1457
+ )
1440
1458
  try:
1441
1459
  return obj_client.get(_request_timeout=REQUEST_TIMEOUT).to_dict()
1442
1460
  except NotFoundError as e:
@@ -19,19 +19,19 @@ class Namespace(Protocol):
19
19
  NS = TypeVar("NS", bound=Namespace)
20
20
 
21
21
 
22
- def filter_namespaces_by_cluster(
22
+ def filter_namespaces_by_cluster[NS: Namespace](
23
23
  namespaces: Iterable[NS], cluster_names: Iterable[str]
24
24
  ) -> list[NS]:
25
25
  return [n for n in namespaces if n.cluster.name in cluster_names]
26
26
 
27
27
 
28
- def filter_namespaces_by_name(
28
+ def filter_namespaces_by_name[NS: Namespace](
29
29
  namespaces: Iterable[NS], namespace_names: Iterable[str]
30
30
  ) -> list[NS]:
31
31
  return [n for n in namespaces if n.name in namespace_names]
32
32
 
33
33
 
34
- def filter_namespaces_by_cluster_and_namespace(
34
+ def filter_namespaces_by_cluster_and_namespace[NS: Namespace](
35
35
  namespaces: Iterable[NS],
36
36
  cluster_names: Iterable[str] | None,
37
37
  namespace_names: Iterable[str] | None,
@@ -47,6 +47,7 @@ SPEC_ATTR_MULTI_AZ = "multi_az"
47
47
  SPEC_ATTR_HYPERSHIFT = "hypershift"
48
48
  SPEC_ATTR_SUBNET_IDS = "subnet_ids"
49
49
  SPEC_ATTR_AVAILABILITY_ZONES = "availability_zones"
50
+ SPEC_ATTR_FIPS = "fips"
50
51
 
51
52
  SPEC_ATTR_NETWORK = "network"
52
53
  IGNORE_NETWORK_TYPE_ATTR = "type"
@@ -177,6 +178,7 @@ class OCMProductOsd(OCMProduct):
177
178
  ],
178
179
  provision_shard_id=provision_shard_id,
179
180
  hypershift=cluster["hypershift"]["enabled"],
181
+ fips=cluster.get("fips") or False,
180
182
  )
181
183
 
182
184
  if not cluster["ccs"]["enabled"]:
@@ -257,6 +259,7 @@ class OCMProductOsd(OCMProduct):
257
259
  if (duwm := cluster.spec.disable_user_workload_monitoring) is not None
258
260
  else True
259
261
  ),
262
+ "fips": cluster.spec.fips,
260
263
  }
261
264
 
262
265
  # Workaround to enable type checks.
@@ -426,6 +429,7 @@ class OCMProductRosa(OCMProduct):
426
429
  subnet_ids=cluster["aws"].get("subnet_ids"),
427
430
  availability_zones=cluster["nodes"].get("availability_zones"),
428
431
  oidc_endpoint_url=oidc_endpoint_url,
432
+ fips=cluster.get("fips") or False,
429
433
  )
430
434
 
431
435
  machine_pools = [
@@ -513,6 +517,7 @@ class OCMProductRosa(OCMProduct):
513
517
  if (duwm := cluster.spec.disable_user_workload_monitoring) is not None
514
518
  else True
515
519
  ),
520
+ "fips": cluster.spec.fips,
516
521
  }
517
522
 
518
523
  provision_shard_id = cluster.spec.provision_shard_id
@@ -701,6 +706,7 @@ class OCMProductHypershift(OCMProduct):
701
706
  availability_zones=cluster["nodes"].get("availability_zones"),
702
707
  hypershift=cluster["hypershift"]["enabled"],
703
708
  oidc_endpoint_url=oidc_endpoint_url,
709
+ fips=cluster.get("fips") or False,
704
710
  )
705
711
 
706
712
  network = OCMClusterNetwork(
@@ -5,7 +5,6 @@ from abc import (
5
5
  from collections.abc import Iterable
6
6
  from dataclasses import dataclass
7
7
  from datetime import (
8
- UTC,
9
8
  datetime,
10
9
  )
11
10
  from enum import Enum
@@ -16,6 +15,8 @@ from typing import (
16
15
 
17
16
  import dateparser
18
17
 
18
+ from reconcile.utils.datetime_util import utc_now
19
+
19
20
 
20
21
  @dataclass
21
22
  class FilterCondition:
@@ -166,17 +167,13 @@ class DateRangeCondition(FilterCondition):
166
167
  return date
167
168
  parsed = dateparser.parse(
168
169
  date,
169
- settings={"RELATIVE_BASE": DateRangeCondition.now()},
170
+ settings={"RELATIVE_BASE": utc_now()},
170
171
  )
171
172
  if parsed is None:
172
173
  raise InvalidFilterError(f"Invalid relative date: {date}")
173
174
 
174
175
  return parsed
175
176
 
176
- @staticmethod
177
- def now() -> datetime:
178
- return datetime.now(tz=UTC)
179
-
180
177
 
181
178
  class InvalidFilterError(Exception):
182
179
  pass
@@ -1,9 +1,7 @@
1
1
  from collections.abc import Generator
2
- from datetime import (
3
- datetime,
4
- timedelta,
5
- )
2
+ from datetime import timedelta
6
3
 
4
+ from reconcile.utils.datetime_util import utc_now
7
5
  from reconcile.utils.ocm.base import (
8
6
  OCMClusterServiceLog,
9
7
  OCMClusterServiceLogCreateModel,
@@ -47,7 +45,7 @@ def create_service_log(
47
45
  .eq("severity", service_log.severity.value)
48
46
  .eq("summary", service_log.summary)
49
47
  .eq("description", service_log.description)
50
- .after("created_at", datetime.utcnow() - dedup_interval),
48
+ .after("created_at", utc_now() - dedup_interval),
51
49
  ),
52
50
  None,
53
51
  )
@@ -4,9 +4,8 @@ from __future__ import annotations
4
4
  import base64
5
5
  import contextlib
6
6
  import copy
7
- import datetime
8
7
  import hashlib
9
- import json
8
+ import logging
10
9
  import re
11
10
  from threading import Lock
12
11
  from typing import TYPE_CHECKING, Any
@@ -15,6 +14,8 @@ import semver
15
14
  from pydantic import BaseModel
16
15
 
17
16
  from reconcile.external_resources.meta import SECRET_UPDATED_AT
17
+ from reconcile.utils.datetime_util import to_utc_seconds_iso_format, utc_now
18
+ from reconcile.utils.json import json_dumps
18
19
  from reconcile.utils.metrics import GaugeMetric
19
20
 
20
21
  if TYPE_CHECKING:
@@ -368,8 +369,8 @@ class OpenshiftResource:
368
369
  annotations[QONTRACT_ANNOTATION_INTEGRATION] = self.integration
369
370
  annotations[QONTRACT_ANNOTATION_INTEGRATION_VERSION] = self.integration_version
370
371
  annotations[QONTRACT_ANNOTATION_SHA256SUM] = sha256sum
371
- now = datetime.datetime.utcnow().replace(microsecond=0).isoformat()
372
- annotations[QONTRACT_ANNOTATION_UPDATE] = now
372
+ now = utc_now()
373
+ annotations[QONTRACT_ANNOTATION_UPDATE] = to_utc_seconds_iso_format(now)
373
374
  if self.caller_name:
374
375
  annotations[QONTRACT_ANNOTATION_CALLER_NAME] = self.caller_name
375
376
 
@@ -530,7 +531,7 @@ class OpenshiftResource:
530
531
 
531
532
  @staticmethod
532
533
  def serialize(body: dict[str, Any]) -> str:
533
- return json.dumps(body, sort_keys=True)
534
+ return json_dumps(body)
534
535
 
535
536
  @staticmethod
536
537
  def calculate_sha256sum(body: str) -> str:
@@ -601,6 +602,10 @@ class ResourceInventory:
601
602
  resource: OpenshiftResource,
602
603
  privileged: bool = False,
603
604
  ) -> None:
605
+ if cluster not in self._clusters:
606
+ logging.error(f"Cluster {cluster} not initialized in ResourceInventory")
607
+ return
608
+
604
609
  if resource.kind_and_group in self._clusters[cluster][namespace]:
605
610
  kind = resource.kind_and_group
606
611
  else:
reconcile/utils/output.py CHANGED
@@ -1,10 +1,11 @@
1
- import json
2
1
  import re
3
2
  from collections.abc import Iterable, Mapping
4
3
 
5
4
  import yaml
6
5
  from tabulate import tabulate
7
6
 
7
+ from reconcile.utils.json import json_dumps
8
+
8
9
 
9
10
  def print_output(
10
11
  options: Mapping[str, str | bool],
@@ -30,7 +31,7 @@ def print_output(
30
31
  )
31
32
  print(formatted_content)
32
33
  elif output == "json":
33
- formatted_content = json.dumps(content)
34
+ formatted_content = json_dumps(content)
34
35
  print(formatted_content)
35
36
  elif output == "yaml":
36
37
  formatted_content = yaml.dump(content)
@@ -3,8 +3,7 @@ from collections.abc import (
3
3
  Callable,
4
4
  Iterable,
5
5
  )
6
- from datetime import datetime as dt
7
- from datetime import timedelta
6
+ from datetime import datetime, timedelta
8
7
  from typing import (
9
8
  Protocol,
10
9
  )
@@ -14,6 +13,7 @@ import requests
14
13
  from pydantic import BaseModel
15
14
  from sretoolbox.utils import retry
16
15
 
16
+ from reconcile.utils.datetime_util import utc_now
17
17
  from reconcile.utils.secret_reader import (
18
18
  HasSecret,
19
19
  SecretReader,
@@ -80,7 +80,7 @@ class PagerDutyApi:
80
80
  def get_pagerduty_users(
81
81
  self, resource_type: str, resource_id: str
82
82
  ) -> list[pypd.User]:
83
- now = dt.utcnow()
83
+ now = utc_now()
84
84
 
85
85
  try:
86
86
  if resource_type == "schedule":
@@ -103,7 +103,7 @@ class PagerDutyApi:
103
103
  self.users.append(user)
104
104
  return user.email.split("@")[0]
105
105
 
106
- def get_schedule_users(self, schedule_id: str, now: dt) -> list[pypd.User]:
106
+ def get_schedule_users(self, schedule_id: str, now: datetime) -> list[pypd.User]:
107
107
  until = now + timedelta(seconds=60)
108
108
  s = pypd.Schedule.fetch(id=schedule_id, since=now, until=until, time_zone="UTC")
109
109
  entries = s["final_schedule"]["rendered_schedule_entries"]
@@ -115,7 +115,7 @@ class PagerDutyApi:
115
115
  ]
116
116
 
117
117
  def get_escalation_policy_users(
118
- self, escalation_policy_id: str, now: dt
118
+ self, escalation_policy_id: str, now: datetime
119
119
  ) -> list[pypd.User]:
120
120
  ep = pypd.EscalationPolicy.fetch(
121
121
  id=escalation_policy_id, since=now, until=now, time_zone="UTC"
@@ -7,7 +7,6 @@ from dataclasses import dataclass
7
7
  from types import ModuleType
8
8
  from typing import (
9
9
  Any,
10
- Generic,
11
10
  Optional,
12
11
  TypeVar,
13
12
  )
@@ -144,7 +143,7 @@ IntegrationClassTypeVar = TypeVar(
144
143
  )
145
144
 
146
145
 
147
- class QontractReconcileIntegration(ABC, Generic[RunParamsTypeVar]):
146
+ class QontractReconcileIntegration[RunParamsTypeVar: RunParams](ABC):
148
147
  """
149
148
  The base class for all integrations. It defines the basic interface to interact
150
149
  with an integration and offers hook methods that allow the integration to opt
@@ -156,7 +156,7 @@ def run_integration_cfg(run_cfg: IntegrationRunConfiguration) -> None:
156
156
  _integration_wet_run(run_cfg.integration)
157
157
 
158
158
 
159
- def _integration_wet_run(
159
+ def _integration_wet_run[RunParamsTypeVar: RunParams](
160
160
  integration: QontractReconcileIntegration[RunParamsTypeVar],
161
161
  ) -> None:
162
162
  """
@@ -165,7 +165,7 @@ def _integration_wet_run(
165
165
  integration.run(False)
166
166
 
167
167
 
168
- def _integration_dry_run(
168
+ def _integration_dry_run[RunParamsTypeVar: RunParams](
169
169
  integration: QontractReconcileIntegration[RunParamsTypeVar],
170
170
  desired_state_diff: DesiredStateDiff | None,
171
171
  ) -> None:
@@ -16,6 +16,7 @@ from reconcile.gql_definitions.fragments.saas_slo_document import (
16
16
  SLODocument,
17
17
  )
18
18
  from reconcile.utils.jenkins_api import JobBuildState
19
+ from reconcile.utils.json import json_dumps
19
20
  from reconcile.utils.oc_connection_parameters import Cluster
20
21
  from reconcile.utils.saasherder.interfaces import (
21
22
  HasParameters,
@@ -422,7 +423,7 @@ class TargetSpec:
422
423
  elif v is False:
423
424
  parameters[k] = "false"
424
425
  elif any(isinstance(v, t) for t in [dict, list, tuple]):
425
- parameters[k] = json.dumps(v)
426
+ parameters[k] = json_dumps(v)
426
427
  return parameters
427
428
 
428
429
  def _collect_secret_parameters(