qontract-reconcile 0.10.2.dev394__py3-none-any.whl → 0.10.2.dev414__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 (308) hide show
  1. {qontract_reconcile-0.10.2.dev394.dist-info → qontract_reconcile-0.10.2.dev414.dist-info}/METADATA +4 -3
  2. {qontract_reconcile-0.10.2.dev394.dist-info → qontract_reconcile-0.10.2.dev414.dist-info}/RECORD +308 -308
  3. reconcile/acs_rbac.py +2 -2
  4. reconcile/aus/advanced_upgrade_service.py +15 -12
  5. reconcile/aus/base.py +9 -13
  6. reconcile/aus/cluster_version_data.py +15 -5
  7. reconcile/aus/models.py +1 -1
  8. reconcile/automated_actions/config/integration.py +15 -3
  9. reconcile/aws_account_manager/integration.py +6 -6
  10. reconcile/aws_account_manager/reconciler.py +3 -3
  11. reconcile/aws_ami_cleanup/integration.py +2 -5
  12. reconcile/aws_ami_share.py +69 -62
  13. reconcile/aws_saml_idp/integration.py +5 -3
  14. reconcile/aws_saml_roles/integration.py +23 -22
  15. reconcile/aws_version_sync/integration.py +6 -12
  16. reconcile/change_owners/bundle.py +3 -3
  17. reconcile/change_owners/change_log_tracking.py +3 -2
  18. reconcile/change_owners/change_owners.py +1 -1
  19. reconcile/dashdotdb_dora.py +1 -1
  20. reconcile/dashdotdb_slo.py +1 -1
  21. reconcile/database_access_manager.py +8 -9
  22. reconcile/dynatrace_token_provider/integration.py +1 -1
  23. reconcile/endpoints_discovery/integration.py +4 -1
  24. reconcile/endpoints_discovery/merge_request.py +1 -1
  25. reconcile/endpoints_discovery/merge_request_manager.py +1 -1
  26. reconcile/external_resources/integration.py +1 -1
  27. reconcile/external_resources/manager.py +19 -7
  28. reconcile/external_resources/metrics.py +1 -1
  29. reconcile/external_resources/model.py +6 -6
  30. reconcile/external_resources/reconciler.py +7 -4
  31. reconcile/external_resources/secrets_sync.py +2 -2
  32. reconcile/external_resources/state.py +56 -14
  33. reconcile/fleet_labeler/integration.py +1 -1
  34. reconcile/gcp_image_mirror.py +2 -2
  35. reconcile/github_org.py +1 -1
  36. reconcile/github_owners.py +4 -0
  37. reconcile/gitlab_members.py +6 -12
  38. reconcile/gitlab_permissions.py +8 -12
  39. reconcile/glitchtip_project_alerts/integration.py +3 -1
  40. reconcile/gql_definitions/acs/acs_instances.py +5 -5
  41. reconcile/gql_definitions/acs/acs_policies.py +5 -5
  42. reconcile/gql_definitions/acs/acs_rbac.py +5 -5
  43. reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py +5 -5
  44. reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py +5 -5
  45. reconcile/gql_definitions/app_interface_metrics_exporter/onboarding_status.py +5 -5
  46. reconcile/gql_definitions/app_sre_tekton_access_revalidation/roles.py +5 -5
  47. reconcile/gql_definitions/app_sre_tekton_access_revalidation/users.py +5 -5
  48. reconcile/gql_definitions/automated_actions/instance.py +46 -7
  49. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +5 -5
  50. reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +5 -5
  51. reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +5 -5
  52. reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +5 -5
  53. reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +5 -5
  54. reconcile/gql_definitions/aws_saml_roles/roles.py +5 -5
  55. reconcile/gql_definitions/aws_version_sync/clusters.py +5 -5
  56. reconcile/gql_definitions/aws_version_sync/namespaces.py +5 -5
  57. reconcile/gql_definitions/change_owners/queries/change_types.py +5 -5
  58. reconcile/gql_definitions/change_owners/queries/self_service_roles.py +5 -5
  59. reconcile/gql_definitions/cluster_auth_rhidp/clusters.py +5 -5
  60. reconcile/gql_definitions/common/alerting_services_settings.py +5 -5
  61. reconcile/gql_definitions/common/app_code_component_repos.py +5 -5
  62. reconcile/gql_definitions/common/app_interface_custom_messages.py +5 -5
  63. reconcile/gql_definitions/common/app_interface_dms_settings.py +5 -5
  64. reconcile/gql_definitions/common/app_interface_repo_settings.py +5 -5
  65. reconcile/gql_definitions/common/app_interface_roles.py +5 -5
  66. reconcile/gql_definitions/common/app_interface_state_settings.py +5 -5
  67. reconcile/gql_definitions/common/app_interface_vault_settings.py +5 -5
  68. reconcile/gql_definitions/common/app_quay_repos_escalation_policies.py +5 -5
  69. reconcile/gql_definitions/common/apps.py +5 -5
  70. reconcile/gql_definitions/common/aws_vpc_requests.py +5 -5
  71. reconcile/gql_definitions/common/aws_vpcs.py +5 -5
  72. reconcile/gql_definitions/common/clusters.py +5 -5
  73. reconcile/gql_definitions/common/clusters_minimal.py +5 -5
  74. reconcile/gql_definitions/common/clusters_with_dms.py +5 -5
  75. reconcile/gql_definitions/common/clusters_with_peering.py +5 -5
  76. reconcile/gql_definitions/common/github_orgs.py +5 -5
  77. reconcile/gql_definitions/common/jira_settings.py +5 -5
  78. reconcile/gql_definitions/common/jiralert_settings.py +5 -5
  79. reconcile/gql_definitions/common/ldap_settings.py +5 -5
  80. reconcile/gql_definitions/common/namespaces.py +5 -5
  81. reconcile/gql_definitions/common/namespaces_minimal.py +7 -5
  82. reconcile/gql_definitions/common/ocm_env_telemeter.py +5 -5
  83. reconcile/gql_definitions/common/ocm_environments.py +5 -5
  84. reconcile/gql_definitions/common/pagerduty_instances.py +5 -5
  85. reconcile/gql_definitions/common/pgp_reencryption_settings.py +5 -5
  86. reconcile/gql_definitions/common/pipeline_providers.py +5 -5
  87. reconcile/gql_definitions/common/quay_instances.py +5 -5
  88. reconcile/gql_definitions/common/quay_orgs.py +5 -5
  89. reconcile/gql_definitions/common/reserved_networks.py +5 -5
  90. reconcile/gql_definitions/common/rhcs_provider_settings.py +5 -5
  91. reconcile/gql_definitions/common/saas_files.py +5 -5
  92. reconcile/gql_definitions/common/saas_target_namespaces.py +5 -5
  93. reconcile/gql_definitions/common/saasherder_settings.py +5 -5
  94. reconcile/gql_definitions/common/slack_workspaces.py +5 -5
  95. reconcile/gql_definitions/common/smtp_client_settings.py +5 -5
  96. reconcile/gql_definitions/common/state_aws_account.py +5 -5
  97. reconcile/gql_definitions/common/users.py +5 -5
  98. reconcile/gql_definitions/common/users_with_paths.py +5 -5
  99. reconcile/gql_definitions/cost_report/app_names.py +5 -5
  100. reconcile/gql_definitions/cost_report/cost_namespaces.py +5 -5
  101. reconcile/gql_definitions/cost_report/settings.py +5 -5
  102. reconcile/gql_definitions/dashdotdb_slo/slo_documents_query.py +5 -5
  103. reconcile/gql_definitions/dynatrace_token_provider/dynatrace_bootstrap_tokens.py +5 -5
  104. reconcile/gql_definitions/dynatrace_token_provider/token_specs.py +5 -5
  105. reconcile/gql_definitions/email_sender/apps.py +5 -5
  106. reconcile/gql_definitions/email_sender/emails.py +5 -5
  107. reconcile/gql_definitions/email_sender/users.py +5 -5
  108. reconcile/gql_definitions/endpoints_discovery/apps.py +5 -5
  109. reconcile/gql_definitions/external_resources/aws_accounts.py +5 -5
  110. reconcile/gql_definitions/external_resources/external_resources_modules.py +5 -5
  111. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +5 -5
  112. reconcile/gql_definitions/external_resources/external_resources_settings.py +5 -5
  113. reconcile/gql_definitions/external_resources/fragments/external_resources_module_overrides.py +5 -5
  114. reconcile/gql_definitions/fleet_labeler/fleet_labels.py +5 -5
  115. reconcile/gql_definitions/fragments/aus_organization.py +5 -5
  116. reconcile/gql_definitions/fragments/aws_account_common.py +5 -5
  117. reconcile/gql_definitions/fragments/aws_account_managed.py +5 -5
  118. reconcile/gql_definitions/fragments/aws_account_sso.py +5 -5
  119. reconcile/gql_definitions/fragments/aws_infra_management_account.py +5 -5
  120. reconcile/gql_definitions/fragments/aws_organization.py +5 -5
  121. reconcile/gql_definitions/fragments/aws_vpc.py +5 -5
  122. reconcile/gql_definitions/fragments/aws_vpc_request.py +5 -5
  123. reconcile/gql_definitions/fragments/container_image_mirror.py +5 -5
  124. reconcile/gql_definitions/fragments/deploy_resources.py +5 -5
  125. reconcile/gql_definitions/fragments/disable.py +5 -5
  126. reconcile/gql_definitions/fragments/email_service.py +5 -5
  127. reconcile/gql_definitions/fragments/email_user.py +5 -5
  128. reconcile/gql_definitions/fragments/jumphost_common_fields.py +5 -5
  129. reconcile/gql_definitions/fragments/membership_source.py +5 -5
  130. reconcile/gql_definitions/fragments/minimal_ocm_organization.py +5 -5
  131. reconcile/gql_definitions/fragments/oc_connection_cluster.py +5 -5
  132. reconcile/gql_definitions/fragments/ocm_environment.py +5 -5
  133. reconcile/gql_definitions/fragments/pipeline_provider_retention.py +5 -5
  134. reconcile/gql_definitions/fragments/prometheus_instance.py +5 -5
  135. reconcile/gql_definitions/fragments/resource_limits_requirements.py +5 -5
  136. reconcile/gql_definitions/fragments/resource_requests_requirements.py +5 -5
  137. reconcile/gql_definitions/fragments/resource_values.py +5 -5
  138. reconcile/gql_definitions/fragments/saas_slo_document.py +5 -5
  139. reconcile/gql_definitions/fragments/saas_target_namespace.py +5 -5
  140. reconcile/gql_definitions/fragments/serviceaccount_token.py +5 -5
  141. reconcile/gql_definitions/fragments/terraform_state.py +5 -5
  142. reconcile/gql_definitions/fragments/upgrade_policy.py +5 -5
  143. reconcile/gql_definitions/fragments/user.py +5 -5
  144. reconcile/gql_definitions/fragments/vault_secret.py +5 -5
  145. reconcile/gql_definitions/gcp/gcp_docker_repos.py +5 -5
  146. reconcile/gql_definitions/gcp/gcp_projects.py +5 -5
  147. reconcile/gql_definitions/gitlab_members/gitlab_instances.py +5 -5
  148. reconcile/gql_definitions/gitlab_members/permissions.py +5 -5
  149. reconcile/gql_definitions/glitchtip/glitchtip_instance.py +5 -5
  150. reconcile/gql_definitions/glitchtip/glitchtip_project.py +5 -5
  151. reconcile/gql_definitions/glitchtip_project_alerts/glitchtip_project.py +5 -5
  152. reconcile/gql_definitions/integrations/integrations.py +5 -5
  153. reconcile/gql_definitions/introspection.json +231 -0
  154. reconcile/gql_definitions/jenkins_configs/jenkins_configs.py +5 -5
  155. reconcile/gql_definitions/jenkins_configs/jenkins_instances.py +5 -5
  156. reconcile/gql_definitions/jira/jira_servers.py +5 -5
  157. reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +5 -5
  158. reconcile/gql_definitions/jumphosts/jumphosts.py +5 -5
  159. reconcile/gql_definitions/ldap_groups/roles.py +5 -5
  160. reconcile/gql_definitions/ldap_groups/settings.py +5 -5
  161. reconcile/gql_definitions/maintenance/maintenances.py +5 -5
  162. reconcile/gql_definitions/membershipsources/roles.py +5 -5
  163. reconcile/gql_definitions/ocm_labels/clusters.py +5 -5
  164. reconcile/gql_definitions/ocm_labels/organizations.py +5 -5
  165. reconcile/gql_definitions/openshift_cluster_bots/clusters.py +5 -5
  166. reconcile/gql_definitions/openshift_groups/managed_groups.py +5 -5
  167. reconcile/gql_definitions/openshift_groups/managed_roles.py +5 -5
  168. reconcile/gql_definitions/openshift_serviceaccount_tokens/tokens.py +5 -5
  169. reconcile/gql_definitions/quay_membership/quay_membership.py +5 -5
  170. reconcile/gql_definitions/rhcs/certs.py +5 -5
  171. reconcile/gql_definitions/rhidp/organizations.py +5 -5
  172. reconcile/gql_definitions/service_dependencies/jenkins_instance_fragment.py +5 -5
  173. reconcile/gql_definitions/service_dependencies/service_dependencies.py +5 -5
  174. reconcile/gql_definitions/sharding/aws_accounts.py +5 -5
  175. reconcile/gql_definitions/sharding/ocm_organization.py +5 -5
  176. reconcile/gql_definitions/skupper_network/site_controller_template.py +5 -5
  177. reconcile/gql_definitions/skupper_network/skupper_networks.py +5 -5
  178. reconcile/gql_definitions/slack_usergroups/clusters.py +5 -5
  179. reconcile/gql_definitions/slack_usergroups/permissions.py +5 -5
  180. reconcile/gql_definitions/slack_usergroups/users.py +5 -5
  181. reconcile/gql_definitions/slo_documents/slo_documents.py +5 -5
  182. reconcile/gql_definitions/status_board/status_board.py +5 -5
  183. reconcile/gql_definitions/statuspage/statuspages.py +5 -5
  184. reconcile/gql_definitions/templating/template_collection.py +5 -5
  185. reconcile/gql_definitions/templating/templates.py +5 -5
  186. reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py +5 -5
  187. reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py +5 -5
  188. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_accounts.py +5 -5
  189. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_resources.py +5 -5
  190. reconcile/gql_definitions/terraform_cloudflare_users/app_interface_setting_cloudflare_and_vault.py +5 -5
  191. reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.py +5 -5
  192. reconcile/gql_definitions/terraform_init/aws_accounts.py +5 -5
  193. reconcile/gql_definitions/terraform_repo/terraform_repo.py +5 -5
  194. reconcile/gql_definitions/terraform_resources/database_access_manager.py +5 -5
  195. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +5 -5
  196. reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +5 -5
  197. reconcile/gql_definitions/unleash_feature_toggles/feature_toggles.py +5 -5
  198. reconcile/gql_definitions/vault_instances/vault_instances.py +5 -5
  199. reconcile/gql_definitions/vault_policies/vault_policies.py +5 -5
  200. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator.py +5 -5
  201. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator_peered_cluster_fragment.py +5 -5
  202. reconcile/integrations_manager.py +3 -3
  203. reconcile/jenkins_worker_fleets.py +9 -8
  204. reconcile/jira_permissions_validator.py +2 -2
  205. reconcile/ldap_groups/integration.py +1 -1
  206. reconcile/ocm/types.py +35 -57
  207. reconcile/ocm_aws_infrastructure_access.py +1 -1
  208. reconcile/ocm_clusters.py +4 -4
  209. reconcile/ocm_labels/integration.py +3 -2
  210. reconcile/ocm_machine_pools.py +23 -23
  211. reconcile/openshift_base.py +53 -2
  212. reconcile/openshift_cluster_bots.py +1 -1
  213. reconcile/openshift_namespace_labels.py +1 -1
  214. reconcile/openshift_namespaces.py +97 -101
  215. reconcile/openshift_resources_base.py +6 -2
  216. reconcile/openshift_rhcs_certs.py +5 -5
  217. reconcile/openshift_rolebindings.py +7 -11
  218. reconcile/openshift_saas_deploy.py +4 -5
  219. reconcile/openshift_saas_deploy_change_tester.py +9 -7
  220. reconcile/openshift_serviceaccount_tokens.py +2 -2
  221. reconcile/openshift_upgrade_watcher.py +1 -1
  222. reconcile/oum/labelset.py +5 -3
  223. reconcile/oum/models.py +1 -4
  224. reconcile/prometheus_rules_tester/integration.py +3 -3
  225. reconcile/quay_mirror.py +1 -1
  226. reconcile/queries.py +6 -0
  227. reconcile/rhidp/common.py +3 -5
  228. reconcile/rhidp/sso_client/base.py +1 -1
  229. reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py +1 -1
  230. reconcile/skupper_network/integration.py +2 -2
  231. reconcile/slack_usergroups.py +31 -11
  232. reconcile/status_board.py +6 -6
  233. reconcile/statuspage/atlassian.py +7 -7
  234. reconcile/statuspage/page.py +4 -9
  235. reconcile/templating/lib/rendering.py +3 -3
  236. reconcile/templating/renderer.py +2 -2
  237. reconcile/terraform_cloudflare_dns.py +3 -3
  238. reconcile/terraform_cloudflare_resources.py +5 -5
  239. reconcile/terraform_cloudflare_users.py +3 -2
  240. reconcile/terraform_init/integration.py +2 -2
  241. reconcile/terraform_repo.py +16 -12
  242. reconcile/terraform_resources.py +6 -6
  243. reconcile/terraform_tgw_attachments.py +20 -18
  244. reconcile/terraform_vpc_resources/integration.py +3 -1
  245. reconcile/typed_queries/cost_report/app_names.py +1 -1
  246. reconcile/typed_queries/cost_report/cost_namespaces.py +2 -2
  247. reconcile/typed_queries/saas_files.py +11 -11
  248. reconcile/typed_queries/status_board.py +2 -2
  249. reconcile/unleash_feature_toggles/integration.py +4 -2
  250. reconcile/utils/acs/base.py +6 -3
  251. reconcile/utils/acs/policies.py +2 -2
  252. reconcile/utils/aws_api.py +51 -20
  253. reconcile/utils/aws_api_typed/organization.py +4 -2
  254. reconcile/utils/deadmanssnitch_api.py +1 -1
  255. reconcile/utils/early_exit_cache.py +8 -10
  256. reconcile/utils/gitlab_api.py +7 -5
  257. reconcile/utils/glitchtip/client.py +6 -2
  258. reconcile/utils/glitchtip/models.py +25 -28
  259. reconcile/utils/gql.py +4 -7
  260. reconcile/utils/instrumented_wrappers.py +1 -1
  261. reconcile/utils/internal_groups/client.py +2 -2
  262. reconcile/utils/internal_groups/models.py +8 -17
  263. reconcile/utils/jinja2/utils.py +2 -5
  264. reconcile/utils/jobcontroller/controller.py +1 -1
  265. reconcile/utils/jobcontroller/models.py +17 -1
  266. reconcile/utils/json.py +39 -1
  267. reconcile/utils/membershipsources/app_interface_resolver.py +4 -2
  268. reconcile/utils/membershipsources/models.py +16 -23
  269. reconcile/utils/membershipsources/resolver.py +4 -2
  270. reconcile/utils/merge_request_manager/merge_request_manager.py +1 -1
  271. reconcile/utils/merge_request_manager/parser.py +4 -4
  272. reconcile/utils/metrics.py +5 -5
  273. reconcile/utils/models.py +304 -82
  274. reconcile/utils/mr/notificator.py +1 -1
  275. reconcile/utils/mr/user_maintenance.py +3 -2
  276. reconcile/utils/oc.py +112 -92
  277. reconcile/utils/ocm/addons.py +0 -1
  278. reconcile/utils/ocm/base.py +17 -20
  279. reconcile/utils/ocm/cluster_groups.py +1 -1
  280. reconcile/utils/ocm/identity_providers.py +2 -2
  281. reconcile/utils/ocm/labels.py +1 -1
  282. reconcile/utils/ocm/products.py +8 -8
  283. reconcile/utils/ocm/service_log.py +1 -1
  284. reconcile/utils/ocm/sre_capability_labels.py +20 -13
  285. reconcile/utils/openshift_resource.py +5 -0
  286. reconcile/utils/pagerduty_api.py +5 -2
  287. reconcile/utils/promotion_state.py +6 -11
  288. reconcile/utils/raw_github_api.py +1 -1
  289. reconcile/utils/rhcsv2_certs.py +1 -4
  290. reconcile/utils/runtime/integration.py +1 -1
  291. reconcile/utils/saasherder/interfaces.py +13 -20
  292. reconcile/utils/saasherder/models.py +23 -20
  293. reconcile/utils/saasherder/saasherder.py +26 -17
  294. reconcile/utils/slack_api.py +2 -2
  295. reconcile/utils/structs.py +1 -1
  296. reconcile/utils/terraform_client.py +1 -1
  297. reconcile/utils/terrascript_aws_client.py +47 -43
  298. reconcile/utils/unleash/server.py +2 -8
  299. reconcile/utils/vault.py +4 -11
  300. reconcile/utils/vcs.py +8 -8
  301. reconcile/vault_replication.py +1 -1
  302. tools/cli_commands/cost_report/cost_management_api.py +3 -3
  303. tools/cli_commands/cost_report/view.py +7 -6
  304. tools/cli_commands/erv2.py +3 -1
  305. tools/qontract_cli.py +6 -5
  306. tools/template_validation.py +3 -1
  307. {qontract_reconcile-0.10.2.dev394.dist-info → qontract_reconcile-0.10.2.dev414.dist-info}/WHEEL +0 -0
  308. {qontract_reconcile-0.10.2.dev394.dist-info → qontract_reconcile-0.10.2.dev414.dist-info}/entry_points.txt +0 -0
reconcile/utils/oc.py CHANGED
@@ -68,7 +68,8 @@ if TYPE_CHECKING:
68
68
  urllib3.disable_warnings()
69
69
 
70
70
  GET_REPLICASET_MAX_ATTEMPTS = 20
71
-
71
+ DEFAULT_GROUP = ""
72
+ PROJECT_KIND = "Project.project.openshift.io"
72
73
 
73
74
  oc_run_execution_counter = Counter(
74
75
  name="oc_run_execution_counter",
@@ -145,6 +146,14 @@ class RequestEntityTooLargeError(Exception):
145
146
  pass
146
147
 
147
148
 
149
+ class KindNotFoundError(Exception):
150
+ pass
151
+
152
+
153
+ class AmbiguousResourceTypeError(Exception):
154
+ pass
155
+
156
+
148
157
  class OCDecorators:
149
158
  @classmethod
150
159
  def process_reconcile_time(cls, function: Callable) -> Callable:
@@ -380,10 +389,7 @@ class OCCli:
380
389
 
381
390
  self.init_projects = init_projects
382
391
  if self.init_projects:
383
- if self.is_kind_supported("Project"):
384
- kind = "Project.project.openshift.io"
385
- else:
386
- kind = "Namespace"
392
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
387
393
  self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
388
394
 
389
395
  self.slow_oc_reconcile_threshold = float(
@@ -453,10 +459,7 @@ class OCCli:
453
459
 
454
460
  self.init_projects = init_projects
455
461
  if self.init_projects:
456
- if self.is_kind_supported("Project"):
457
- kind = "Project.project.openshift.io"
458
- else:
459
- kind = "Namespace"
462
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
460
463
  self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
461
464
 
462
465
  self.slow_oc_reconcile_threshold = float(
@@ -637,11 +640,9 @@ class OCCli:
637
640
  def project_exists(self, name: str) -> bool:
638
641
  if name in self.projects:
639
642
  return True
643
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
640
644
  try:
641
- if self.is_kind_supported("Project"):
642
- self.get(None, "Project.project.openshift.io", name)
643
- else:
644
- self.get(None, "Namespace", name)
645
+ self.get(None, kind, name)
645
646
  except StatusCodeError as e:
646
647
  if "NotFound" in str(e):
647
648
  return False
@@ -650,7 +651,7 @@ class OCCli:
650
651
 
651
652
  @OCDecorators.process_reconcile_time
652
653
  def new_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
653
- if self.is_kind_supported("Project"):
654
+ if self.is_kind_supported(PROJECT_KIND):
654
655
  cmd = ["new-project", namespace]
655
656
  else:
656
657
  cmd = ["create", "namespace", namespace]
@@ -666,7 +667,7 @@ class OCCli:
666
667
 
667
668
  @OCDecorators.process_reconcile_time
668
669
  def delete_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
669
- if self.is_kind_supported("Project"):
670
+ if self.is_kind_supported(PROJECT_KIND):
670
671
  cmd = ["delete", "project", namespace]
671
672
  else:
672
673
  cmd = ["delete", "namespace", namespace]
@@ -715,9 +716,9 @@ class OCCli:
715
716
 
716
717
  def sa_get_token(self, namespace: str, name: str) -> str:
717
718
  cmd = ["sa", "-n", namespace, "get-token", name]
718
- return self._run(cmd)
719
+ return self._run(cmd).decode("utf-8")
719
720
 
720
- def get_api_resources(self) -> dict[str, Any]:
721
+ def get_api_resources(self) -> dict[str, list[OCCliApiResource]]:
721
722
  with self.api_resources_lock:
722
723
  if not self.api_resources:
723
724
  cmd = ["api-resources", "--no-headers"]
@@ -1187,7 +1188,7 @@ class OCCli:
1187
1188
  def _run_json(
1188
1189
  self, cmd: list[str], allow_not_found: bool = False
1189
1190
  ) -> dict[str, Any]:
1190
- out = self._run(cmd, allow_not_found=allow_not_found)
1191
+ out = self._run(cmd, allow_not_found=allow_not_found).decode("utf-8")
1191
1192
 
1192
1193
  try:
1193
1194
  out_json = json.loads(out)
@@ -1196,76 +1197,90 @@ class OCCli:
1196
1197
 
1197
1198
  return out_json
1198
1199
 
1199
- def _parse_kind(self, kind_name: str) -> tuple[str, str]:
1200
- # This is a provisional solution while we work in redefining
1201
- # the api resources initialization.
1202
- if not self.api_resources:
1203
- self.get_api_resources()
1200
+ def parse_kind(self, kind: str) -> tuple[str, str, str]:
1201
+ """Parse a Kubernetes kind string into its components.
1204
1202
 
1205
- kind_group = kind_name.split(".", 1)
1206
- kind = kind_group[0]
1207
- if kind in self.api_resources:
1208
- group_version = self.api_resources[kind][0].group_version
1209
- else:
1210
- raise StatusCodeError(f"{self.server}: {kind} does not exist")
1211
-
1212
- # if a kind_group has more than 1 entry than the kind_name is in
1213
- # the format kind.apigroup. Find the apigroup/version that matches
1214
- # the apigroup passed with the kind_name
1215
- if len(kind_group) > 1:
1216
- apigroup_override = kind_group[1]
1217
- find = False
1218
- for gv in self.api_resources[kind]:
1219
- if apigroup_override == gv.group:
1220
- if not gv.group:
1221
- group_version = gv.api_version
1222
- else:
1223
- group_version = f"{gv.group}/{gv.api_version}"
1224
- find = True
1225
- break
1203
+ Supports three formats:
1204
+ - kind
1205
+ - kind.group.whatever
1206
+ - kind.group.whatever/version
1226
1207
 
1227
- if not find:
1228
- raise StatusCodeError(
1229
- f"{self.server}: {apigroup_override} does not have kind {kind}"
1230
- )
1231
- 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
1232
1235
 
1233
1236
  def is_kind_supported(self, kind: str) -> bool:
1234
- # This is a provisional solution while we work in redefining
1235
- # the api resources initialization.
1236
- if not self.api_resources:
1237
- self.get_api_resources()
1237
+ """Returns True if the given kind is supported by the cluster, False otherwise.
1238
1238
 
1239
- if "." in kind:
1240
- try:
1241
- self._parse_kind(kind)
1242
- return True
1243
- except StatusCodeError:
1244
- return False
1245
- else:
1246
- 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
1247
1245
 
1248
1246
  def is_kind_namespaced(self, kind: str) -> bool:
1249
- # This is a provisional solution while we work in redefining
1250
- # 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
+
1251
1258
  if not self.api_resources:
1252
- self.get_api_resources()
1259
+ raise RuntimeError("API resources not initialized")
1260
+
1261
+ kind, group, _ = self.parse_kind(kind)
1253
1262
 
1254
- kg = kind.split(".", 1)
1255
- kind = kg[0]
1263
+ if not (resources := self.api_resources.get(kind)):
1264
+ # the kind not found at all
1265
+ raise KindNotFoundError(f"Unsupported resource type: {kind}")
1256
1266
 
1257
- # Same Kinds might exist in different api groups
1258
- kind_resources = self.api_resources.get(kind)
1259
- if not kind_resources:
1260
- raise StatusCodeError(f"Kind {kind} does not exist in the ApiServer")
1267
+ if len(resources) == 1 and group == DEFAULT_GROUP:
1268
+ return resources[0]
1261
1269
 
1262
- if len(kg) > 1:
1263
- group = kg[1]
1264
- for r in kind_resources:
1265
- if group == r.group:
1266
- return r.namespaced
1267
- raise StatusCodeError(f"Kind: {kind} does nod exist in the ApiServer")
1268
- return kind_resources[0].namespaced
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}")
1269
1284
 
1270
1285
 
1271
1286
  REQUEST_TIMEOUT = 60
@@ -1305,20 +1320,19 @@ class OCNative(OCCli):
1305
1320
 
1306
1321
  server = connection_parameters.server_url
1307
1322
 
1308
- if server:
1309
- self.client = self._get_client(server, token)
1310
- self.api_resources = self.get_api_resources()
1323
+ if not server:
1324
+ raise Exception("Server name is required!")
1311
1325
 
1312
- else:
1313
- raise Exception("A method relies on client/api_kind_version to be set")
1326
+ if not token:
1327
+ raise Exception("Token is required!")
1328
+
1329
+ self.client = self._get_client(server, token)
1330
+ self.api_resources = self.get_api_resources()
1314
1331
 
1315
1332
  self.projects = set()
1316
1333
  self.init_projects = init_projects
1317
1334
  if self.init_projects:
1318
- if self.is_kind_supported("Project"):
1319
- kind = "Project.project.openshift.io"
1320
- else:
1321
- kind = "Namespace"
1335
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
1322
1336
  self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
1323
1337
 
1324
1338
  def __enter__(self) -> OCNative:
@@ -1368,8 +1382,10 @@ class OCNative(OCCli):
1368
1382
 
1369
1383
  @retry(max_attempts=5, exceptions=(ServerTimeoutError))
1370
1384
  def get_items(self, kind: str, **kwargs: Any) -> list[dict[str, Any]]:
1371
- k, group_version = self._parse_kind(kind)
1372
- obj_client = self._get_obj_client(group_version=group_version, kind=k)
1385
+ resource = self.get_api_resource(kind)
1386
+ obj_client = self._get_obj_client(
1387
+ group_version=resource.group_version, kind=resource.kind
1388
+ )
1373
1389
 
1374
1390
  namespace = ""
1375
1391
  if "namespace" in kwargs:
@@ -1421,8 +1437,10 @@ class OCNative(OCCli):
1421
1437
  name: str | None = None,
1422
1438
  allow_not_found: bool = False,
1423
1439
  ) -> dict[str, Any]:
1424
- k, group_version = self._parse_kind(kind)
1425
- obj_client = self._get_obj_client(group_version=group_version, kind=k)
1440
+ resource = self.get_api_resource(kind)
1441
+ obj_client = self._get_obj_client(
1442
+ group_version=resource.group_version, kind=resource.kind
1443
+ )
1426
1444
  try:
1427
1445
  obj = obj_client.get(
1428
1446
  name=name,
@@ -1436,8 +1454,10 @@ class OCNative(OCCli):
1436
1454
  raise StatusCodeError(f"[{self.server}]: {e}") from None
1437
1455
 
1438
1456
  def get_all(self, kind: str, all_namespaces: bool = False) -> dict[str, Any]:
1439
- k, group_version = self._parse_kind(kind)
1440
- obj_client = self._get_obj_client(group_version=group_version, kind=k)
1457
+ resource = self.get_api_resource(kind)
1458
+ obj_client = self._get_obj_client(
1459
+ group_version=resource.group_version, kind=resource.kind
1460
+ )
1441
1461
  try:
1442
1462
  return obj_client.get(_request_timeout=REQUEST_TIMEOUT).to_dict()
1443
1463
  except NotFoundError as e:
@@ -188,7 +188,6 @@ class AddonServiceV2(AddonService):
188
188
  next_run=policy.get("next_run"),
189
189
  version=policy["version"],
190
190
  state=policy.get("state"),
191
- addon_service=self,
192
191
  )
193
192
  )
194
193
 
@@ -141,16 +141,16 @@ class OCMClusterAWSOperatorRole(BaseModel):
141
141
 
142
142
 
143
143
  class OCMAWSSTS(OCMClusterFlag):
144
- role_arn: str | None
145
- support_role_arn: str | None
146
- oidc_endpoint_url: str | None
147
- operator_iam_roles: list[OCMClusterAWSOperatorRole] | None
148
- instance_iam_roles: dict[str, str] | None
149
- operator_role_prefix: str | None
144
+ role_arn: str | None = None
145
+ support_role_arn: str | None = None
146
+ oidc_endpoint_url: str | None = None
147
+ operator_iam_roles: list[OCMClusterAWSOperatorRole] | None = None
148
+ instance_iam_roles: dict[str, str] | None = None
149
+ operator_role_prefix: str | None = None
150
150
 
151
151
 
152
152
  class OCMClusterAWSSettings(BaseModel):
153
- sts: OCMAWSSTS | None
153
+ sts: OCMAWSSTS | None = None
154
154
 
155
155
  @property
156
156
  def sts_enabled(self) -> bool:
@@ -264,21 +264,21 @@ class OCMCluster(BaseModel):
264
264
  product: OCMModelLink
265
265
  identity_providers: OCMCollectionLink
266
266
 
267
- aws: OCMClusterAWSSettings | None
267
+ aws: OCMClusterAWSSettings | None = None
268
268
 
269
269
  version: OCMClusterVersion
270
270
 
271
271
  hypershift: OCMClusterFlag
272
272
 
273
- console: OCMClusterConsole | None
273
+ console: OCMClusterConsole | None = None
274
274
 
275
- api: OCMClusterAPI | None
275
+ api: OCMClusterAPI | None = None
276
276
 
277
- dns: OCMClusterDns | None
277
+ dns: OCMClusterDns | None = None
278
278
 
279
- external_configuration: OCMExternalConfiguration | None
279
+ external_configuration: OCMExternalConfiguration | None = None
280
280
 
281
- external_auth_config: OCMExternalAuthConfig | None
281
+ external_auth_config: OCMExternalAuthConfig | None = None
282
282
 
283
283
  def minor_version(self) -> str:
284
284
  version_info = parse_semver(self.version.raw_id)
@@ -570,15 +570,12 @@ class OCMOIdentityProviderGithub(OCMOIdentityProvider):
570
570
  )
571
571
 
572
572
 
573
- class OCMOIdentityProviderOidcOpenIdClaims(BaseModel):
573
+ class OCMOIdentityProviderOidcOpenIdClaims(BaseModel, frozen=True):
574
574
  email: list[str]
575
575
  name: list[str] = []
576
576
  preferred_username: list[str]
577
577
  groups: list[str] = []
578
578
 
579
- class Config:
580
- frozen = True
581
-
582
579
 
583
580
  class OCMOIdentityProviderOidcOpenId(BaseModel):
584
581
  client_id: str
@@ -618,11 +615,11 @@ class OCMAddonUpgradePolicy(BaseModel):
618
615
  id: str
619
616
  addon_id: str
620
617
  cluster_id: str
621
- next_run: str | None
622
- schedule: str | None
618
+ next_run: str | None = None
619
+ schedule: str | None = None
623
620
  schedule_type: str
624
621
  version: str
625
- state: str | None
622
+ state: str | None = None
626
623
 
627
624
 
628
625
  def build_label_container(
@@ -17,7 +17,7 @@ def add_user_to_cluster_group(
17
17
  """
18
18
  ocm_api.post(
19
19
  build_cluster_group_users_url(cluster_id, group),
20
- OCMClusterUser(id=user_name).dict(by_alias=True),
20
+ OCMClusterUser(id=user_name).model_dump(by_alias=True),
21
21
  )
22
22
 
23
23
 
@@ -42,7 +42,7 @@ def add_identity_provider(
42
42
  )
43
43
  ocm_api.post(
44
44
  api_path=ocm_cluster.identity_providers.href,
45
- data=idp.dict(by_alias=True, exclude_none=True),
45
+ data=idp.model_dump(by_alias=True, exclude_none=True),
46
46
  )
47
47
 
48
48
 
@@ -55,7 +55,7 @@ def update_identity_provider(
55
55
  raise ValueError(f"IDP {idp.name} does not have a href!")
56
56
  ocm_api.patch(
57
57
  api_path=idp.href,
58
- data=idp.dict(by_alias=True, exclude_none=True, exclude={"name"}),
58
+ data=idp.model_dump(by_alias=True, exclude_none=True, exclude={"name"}),
59
59
  )
60
60
 
61
61
 
@@ -159,7 +159,7 @@ def build_container_for_prefix(
159
159
 
160
160
  return LabelContainer(
161
161
  labels={
162
- strip_prefix_if_needed(label.key): label.copy(
162
+ strip_prefix_if_needed(label.key): label.model_copy(
163
163
  update={"key": strip_prefix_if_needed(label.key)}
164
164
  )
165
165
  for label in container.labels.values()
@@ -178,11 +178,11 @@ class OCMProductOsd(OCMProduct):
178
178
  ],
179
179
  provision_shard_id=provision_shard_id,
180
180
  hypershift=cluster["hypershift"]["enabled"],
181
- fips=cluster.get("fips"),
181
+ fips=cluster.get("fips") or False,
182
182
  )
183
183
 
184
184
  if not cluster["ccs"]["enabled"]:
185
- cluster_spec_data = spec.dict()
185
+ cluster_spec_data = spec.model_dump()
186
186
  cluster_spec_data["storage"] = (
187
187
  cluster["storage_quota"]["value"] // BYTES_IN_GIGABYTE
188
188
  )
@@ -229,7 +229,7 @@ class OCMProductOsd(OCMProduct):
229
229
  "compute_machine_type": {"id": default_machine_pool.instance_type},
230
230
  }
231
231
  if default_machine_pool.autoscale is not None:
232
- spec["autoscale_compute"] = default_machine_pool.autoscale.dict()
232
+ spec["autoscale_compute"] = default_machine_pool.autoscale.model_dump()
233
233
  else:
234
234
  spec["compute"] = default_machine_pool.replicas
235
235
  return spec
@@ -259,7 +259,7 @@ class OCMProductOsd(OCMProduct):
259
259
  if (duwm := cluster.spec.disable_user_workload_monitoring) is not None
260
260
  else True
261
261
  ),
262
- "fips": bool(cluster.spec.fips),
262
+ "fips": cluster.spec.fips,
263
263
  }
264
264
 
265
265
  # Workaround to enable type checks.
@@ -429,7 +429,7 @@ class OCMProductRosa(OCMProduct):
429
429
  subnet_ids=cluster["aws"].get("subnet_ids"),
430
430
  availability_zones=cluster["nodes"].get("availability_zones"),
431
431
  oidc_endpoint_url=oidc_endpoint_url,
432
- fips=cluster.get("fips"),
432
+ fips=cluster.get("fips") or False,
433
433
  )
434
434
 
435
435
  machine_pools = [
@@ -474,7 +474,7 @@ class OCMProductRosa(OCMProduct):
474
474
  "compute_machine_type": {"id": default_machine_pool.instance_type},
475
475
  }
476
476
  if default_machine_pool.autoscale is not None:
477
- spec["autoscale_compute"] = default_machine_pool.autoscale.dict()
477
+ spec["autoscale_compute"] = default_machine_pool.autoscale.model_dump()
478
478
  else:
479
479
  spec["compute"] = default_machine_pool.replicas
480
480
  return spec
@@ -517,7 +517,7 @@ class OCMProductRosa(OCMProduct):
517
517
  if (duwm := cluster.spec.disable_user_workload_monitoring) is not None
518
518
  else True
519
519
  ),
520
- "fips": bool(cluster.spec.fips),
520
+ "fips": cluster.spec.fips,
521
521
  }
522
522
 
523
523
  provision_shard_id = cluster.spec.provision_shard_id
@@ -706,7 +706,7 @@ class OCMProductHypershift(OCMProduct):
706
706
  availability_zones=cluster["nodes"].get("availability_zones"),
707
707
  hypershift=cluster["hypershift"]["enabled"],
708
708
  oidc_endpoint_url=oidc_endpoint_url,
709
- fips=cluster.get("fips"),
709
+ fips=cluster.get("fips") or False,
710
710
  )
711
711
 
712
712
  network = OCMClusterNetwork(
@@ -55,6 +55,6 @@ def create_service_log(
55
55
  return OCMClusterServiceLog(
56
56
  **ocm_api.post(
57
57
  api_path=CLUSTER_SERVICE_LOGS_CREATE_ENDPOINT,
58
- data=service_log.dict(by_alias=True),
58
+ data=service_log.model_dump(by_alias=True),
59
59
  )
60
60
  )
@@ -1,7 +1,7 @@
1
1
  from typing import Any
2
2
 
3
- from pydantic import Field
4
- from pydantic.fields import ModelField
3
+ from pydantic import WithJsonSchema
4
+ from pydantic.fields import FieldInfo
5
5
 
6
6
  from reconcile.utils.ocm.base import (
7
7
  LabelContainer,
@@ -22,11 +22,11 @@ def sre_capability_label_key(
22
22
  return f"sre-capabilities.{sre_capability}.{config_atom}"
23
23
 
24
24
 
25
- def labelset_groupfield(group_prefix: str) -> Any:
25
+ def labelset_groupfield(group_prefix: str) -> WithJsonSchema:
26
26
  """
27
27
  Helper function to build the FieldMeta for a labelset field that groups labels.
28
28
  """
29
- return Field(group_by_prefix=group_prefix)
29
+ return WithJsonSchema({"group_by_prefix": group_prefix})
30
30
 
31
31
 
32
32
  def build_labelset(
@@ -36,16 +36,23 @@ def build_labelset(
36
36
  Instantiates a dataclass from a set of labels.
37
37
  """
38
38
  raw_data = {
39
- field.alias: _labelset_field_value(labels, field)
40
- for field in dataclass.__fields__.values()
39
+ field.alias or name: _labelset_field_value(labels, name, field)
40
+ for name, field in dataclass.model_fields.items()
41
41
  }
42
42
  return dataclass(**raw_data)
43
43
 
44
44
 
45
- def _labelset_field_value(labels: LabelContainer, field: ModelField) -> Any | None:
46
- key_prefix = field.field_info.extra.get("group_by_prefix")
47
- if key_prefix:
48
- return build_container_for_prefix(
49
- labels, key_prefix, strip_key_prefix=True
50
- ).get_values_dict()
51
- return labels.get_label_value(field.alias)
45
+ def _labelset_field_value(
46
+ labels: LabelContainer, name: str, field: FieldInfo
47
+ ) -> Any | None:
48
+ schema = next((m for m in field.metadata if isinstance(m, WithJsonSchema)), None)
49
+ if (
50
+ schema is None
51
+ or not schema.json_schema
52
+ or "group_by_prefix" not in schema.json_schema
53
+ ):
54
+ return labels.get_label_value(field.alias or name)
55
+
56
+ return build_container_for_prefix(
57
+ labels, schema.json_schema["group_by_prefix"], strip_key_prefix=True
58
+ ).get_values_dict()
@@ -5,6 +5,7 @@ import base64
5
5
  import contextlib
6
6
  import copy
7
7
  import hashlib
8
+ import logging
8
9
  import re
9
10
  from threading import Lock
10
11
  from typing import TYPE_CHECKING, Any
@@ -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:
@@ -52,10 +52,13 @@ class PagerDutyTarget(Protocol):
52
52
  which must be implemented by a class to be compatible."""
53
53
 
54
54
  name: str
55
- instance: PagerDutyInstance
56
55
  escalation_policy_id: str | None
57
56
  schedule_id: str | None
58
57
 
58
+ @property
59
+ def instance(self) -> PagerDutyInstance:
60
+ pass
61
+
59
62
 
60
63
  class PagerDutyConfig(BaseModel):
61
64
  """PagerDuty Config."""
@@ -189,7 +192,7 @@ def get_pagerduty_name(user: PagerDutyUser) -> str:
189
192
  return user.pagerduty_username or user.org_username
190
193
 
191
194
 
192
- @retry(no_retry_exceptions=PagerDutyTargetError)
195
+ @retry(no_retry_exceptions=(PagerDutyTargetError,))
193
196
  def get_usernames_from_pagerduty(
194
197
  pagerduties: Iterable[PagerDutyTarget],
195
198
  users: Iterable[PagerDutyUser],
@@ -3,13 +3,12 @@ from collections import defaultdict
3
3
 
4
4
  from pydantic import (
5
5
  BaseModel,
6
- Extra,
7
6
  )
8
7
 
9
8
  from reconcile.utils.state import State
10
9
 
11
10
 
12
- class PromotionData(BaseModel):
11
+ class PromotionData(BaseModel, extra="forbid"):
13
12
  """
14
13
  A class that strictly corresponds to the json stored in S3.
15
14
 
@@ -20,17 +19,13 @@ class PromotionData(BaseModel):
20
19
 
21
20
  # The success is primarily used for SAPM auto-promotions
22
21
  success: bool
23
- target_config_hash: str | None
24
- saas_file: str | None
25
- check_in: str | None
22
+ target_config_hash: str | None = None
23
+ saas_file: str | None = None
24
+ check_in: str | None = None
26
25
  # Whether this promotion has ever succeeded
27
26
  # Note, this shouldnt be overridden on subsequent promotions of same ref
28
27
  # This attribute is primarily used by saasherder validations
29
- has_succeeded_once: bool | None
30
-
31
- class Config:
32
- smart_union = True
33
- extra = Extra.forbid
28
+ has_succeeded_once: bool | None = None
34
29
 
35
30
 
36
31
  class PromotionState:
@@ -107,5 +102,5 @@ class PromotionState:
107
102
  self, sha: str, channel: str, target_uid: str, data: PromotionData
108
103
  ) -> None:
109
104
  state_key_v2 = f"promotions_v2/{channel}/{target_uid}/{sha}"
110
- self._state.add(state_key_v2, data.dict(), force=True)
105
+ self._state.add(state_key_v2, data.model_dump(), force=True)
111
106
  logging.info("Uploaded %s to %s", data, state_key_v2)