qontract-reconcile 0.10.2.dev361__py3-none-any.whl → 0.10.2.dev474__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 (371) hide show
  1. {qontract_reconcile-0.10.2.dev361.dist-info → qontract_reconcile-0.10.2.dev474.dist-info}/METADATA +14 -13
  2. {qontract_reconcile-0.10.2.dev361.dist-info → qontract_reconcile-0.10.2.dev474.dist-info}/RECORD +371 -364
  3. {qontract_reconcile-0.10.2.dev361.dist-info → qontract_reconcile-0.10.2.dev474.dist-info}/WHEEL +1 -1
  4. reconcile/acs_rbac.py +2 -2
  5. reconcile/aus/advanced_upgrade_service.py +18 -12
  6. reconcile/aus/aus_sts_gate_handler.py +59 -0
  7. reconcile/aus/base.py +137 -34
  8. reconcile/aus/cluster_version_data.py +15 -5
  9. reconcile/aus/models.py +3 -1
  10. reconcile/aus/ocm_addons_upgrade_scheduler_org.py +1 -0
  11. reconcile/aus/ocm_upgrade_scheduler.py +8 -1
  12. reconcile/aus/ocm_upgrade_scheduler_org.py +20 -5
  13. reconcile/aus/version_gate_approver.py +1 -16
  14. reconcile/aus/version_gates/sts_version_gate_handler.py +5 -72
  15. reconcile/automated_actions/config/integration.py +16 -4
  16. reconcile/aws_account_manager/integration.py +21 -9
  17. reconcile/aws_account_manager/reconciler.py +3 -3
  18. reconcile/aws_account_manager/utils.py +1 -1
  19. reconcile/aws_ami_cleanup/integration.py +8 -12
  20. reconcile/aws_ami_share.py +69 -62
  21. reconcile/aws_cloudwatch_log_retention/integration.py +155 -126
  22. reconcile/aws_ecr_image_pull_secrets.py +1 -1
  23. reconcile/aws_iam_keys.py +1 -0
  24. reconcile/aws_saml_idp/integration.py +12 -4
  25. reconcile/aws_saml_roles/integration.py +30 -23
  26. reconcile/aws_version_sync/integration.py +6 -12
  27. reconcile/change_owners/README.md +1 -1
  28. reconcile/change_owners/bundle.py +3 -3
  29. reconcile/change_owners/change_log_tracking.py +3 -2
  30. reconcile/change_owners/change_owners.py +108 -42
  31. reconcile/change_owners/decision.py +1 -1
  32. reconcile/change_owners/diff.py +0 -2
  33. reconcile/checkpoint.py +11 -3
  34. reconcile/cli.py +94 -11
  35. reconcile/dashdotdb_dora.py +5 -12
  36. reconcile/dashdotdb_slo.py +1 -1
  37. reconcile/database_access_manager.py +123 -117
  38. reconcile/dynatrace_token_provider/integration.py +1 -1
  39. reconcile/endpoints_discovery/integration.py +4 -1
  40. reconcile/endpoints_discovery/merge_request.py +1 -1
  41. reconcile/endpoints_discovery/merge_request_manager.py +8 -8
  42. reconcile/external_resources/factories.py +4 -6
  43. reconcile/external_resources/integration.py +1 -1
  44. reconcile/external_resources/manager.py +8 -6
  45. reconcile/external_resources/meta.py +0 -1
  46. reconcile/external_resources/metrics.py +1 -1
  47. reconcile/external_resources/model.py +19 -15
  48. reconcile/external_resources/reconciler.py +7 -4
  49. reconcile/external_resources/secrets_sync.py +6 -10
  50. reconcile/external_resources/state.py +26 -16
  51. reconcile/fleet_labeler/integration.py +1 -1
  52. reconcile/gabi_authorized_users.py +5 -2
  53. reconcile/gcp_image_mirror.py +2 -2
  54. reconcile/github_org.py +1 -1
  55. reconcile/github_owners.py +4 -0
  56. reconcile/gitlab_housekeeping.py +13 -15
  57. reconcile/gitlab_members.py +6 -12
  58. reconcile/gitlab_owners.py +15 -11
  59. reconcile/gitlab_permissions.py +8 -12
  60. reconcile/glitchtip_project_alerts/integration.py +3 -1
  61. reconcile/gql_definitions/acs/acs_instances.py +5 -5
  62. reconcile/gql_definitions/acs/acs_policies.py +5 -5
  63. reconcile/gql_definitions/acs/acs_rbac.py +5 -5
  64. reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py +5 -5
  65. reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py +5 -5
  66. reconcile/gql_definitions/app_interface_metrics_exporter/onboarding_status.py +5 -5
  67. reconcile/gql_definitions/app_sre_tekton_access_revalidation/roles.py +5 -5
  68. reconcile/gql_definitions/app_sre_tekton_access_revalidation/users.py +5 -5
  69. reconcile/gql_definitions/automated_actions/instance.py +46 -7
  70. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +14 -5
  71. reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +15 -5
  72. reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +27 -66
  73. reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +15 -5
  74. reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +15 -5
  75. reconcile/gql_definitions/aws_saml_roles/roles.py +5 -5
  76. reconcile/gql_definitions/aws_version_sync/clusters.py +5 -5
  77. reconcile/gql_definitions/aws_version_sync/namespaces.py +5 -5
  78. reconcile/gql_definitions/change_owners/queries/change_types.py +5 -5
  79. reconcile/gql_definitions/change_owners/queries/self_service_roles.py +5 -5
  80. reconcile/gql_definitions/cluster_auth_rhidp/clusters.py +5 -5
  81. reconcile/gql_definitions/common/alerting_services_settings.py +5 -5
  82. reconcile/gql_definitions/common/app_code_component_repos.py +5 -5
  83. reconcile/gql_definitions/common/app_interface_custom_messages.py +5 -5
  84. reconcile/gql_definitions/common/app_interface_dms_settings.py +5 -5
  85. reconcile/gql_definitions/common/app_interface_repo_settings.py +5 -5
  86. reconcile/gql_definitions/common/app_interface_roles.py +5 -5
  87. reconcile/gql_definitions/common/app_interface_state_settings.py +5 -5
  88. reconcile/gql_definitions/common/app_interface_vault_settings.py +5 -5
  89. reconcile/gql_definitions/common/app_quay_repos_escalation_policies.py +5 -5
  90. reconcile/gql_definitions/common/apps.py +5 -5
  91. reconcile/gql_definitions/common/aws_vpc_requests.py +18 -5
  92. reconcile/gql_definitions/common/aws_vpcs.py +5 -5
  93. reconcile/gql_definitions/common/clusters.py +7 -5
  94. reconcile/gql_definitions/common/clusters_minimal.py +5 -5
  95. reconcile/gql_definitions/common/clusters_with_dms.py +5 -5
  96. reconcile/gql_definitions/common/clusters_with_peering.py +5 -5
  97. reconcile/gql_definitions/common/github_orgs.py +5 -5
  98. reconcile/gql_definitions/common/jira_settings.py +5 -5
  99. reconcile/gql_definitions/common/jiralert_settings.py +5 -5
  100. reconcile/gql_definitions/common/ldap_settings.py +5 -5
  101. reconcile/gql_definitions/common/namespaces.py +5 -5
  102. reconcile/gql_definitions/common/namespaces_minimal.py +7 -5
  103. reconcile/gql_definitions/common/ocm_env_telemeter.py +5 -5
  104. reconcile/gql_definitions/common/ocm_environments.py +5 -5
  105. reconcile/gql_definitions/common/pagerduty_instances.py +5 -5
  106. reconcile/gql_definitions/common/pgp_reencryption_settings.py +5 -5
  107. reconcile/gql_definitions/common/pipeline_providers.py +5 -5
  108. reconcile/gql_definitions/common/quay_instances.py +5 -5
  109. reconcile/gql_definitions/common/quay_orgs.py +5 -5
  110. reconcile/gql_definitions/common/reserved_networks.py +5 -5
  111. reconcile/gql_definitions/common/rhcs_provider_settings.py +5 -5
  112. reconcile/gql_definitions/common/saas_files.py +5 -5
  113. reconcile/gql_definitions/common/saas_target_namespaces.py +5 -5
  114. reconcile/gql_definitions/common/saasherder_settings.py +5 -5
  115. reconcile/gql_definitions/common/slack_workspaces.py +5 -5
  116. reconcile/gql_definitions/common/smtp_client_settings.py +5 -5
  117. reconcile/gql_definitions/common/state_aws_account.py +5 -5
  118. reconcile/gql_definitions/common/users.py +5 -5
  119. reconcile/gql_definitions/common/users_with_paths.py +5 -5
  120. reconcile/gql_definitions/cost_report/app_names.py +5 -5
  121. reconcile/gql_definitions/cost_report/cost_namespaces.py +5 -5
  122. reconcile/gql_definitions/cost_report/settings.py +5 -5
  123. reconcile/gql_definitions/dashdotdb_slo/slo_documents_query.py +5 -5
  124. reconcile/gql_definitions/dynatrace_token_provider/dynatrace_bootstrap_tokens.py +5 -5
  125. reconcile/gql_definitions/dynatrace_token_provider/token_specs.py +5 -5
  126. reconcile/gql_definitions/email_sender/apps.py +5 -5
  127. reconcile/gql_definitions/email_sender/emails.py +5 -5
  128. reconcile/gql_definitions/email_sender/users.py +5 -5
  129. reconcile/gql_definitions/endpoints_discovery/apps.py +5 -5
  130. reconcile/gql_definitions/external_resources/aws_accounts.py +5 -5
  131. reconcile/gql_definitions/external_resources/external_resources_modules.py +5 -5
  132. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +38 -7
  133. reconcile/gql_definitions/external_resources/external_resources_settings.py +5 -5
  134. reconcile/gql_definitions/external_resources/fragments/external_resources_module_overrides.py +5 -5
  135. reconcile/gql_definitions/fleet_labeler/fleet_labels.py +5 -5
  136. reconcile/gql_definitions/fragments/aus_organization.py +5 -5
  137. reconcile/gql_definitions/fragments/aws_account_common.py +7 -5
  138. reconcile/gql_definitions/fragments/aws_account_managed.py +5 -5
  139. reconcile/gql_definitions/fragments/aws_account_sso.py +5 -5
  140. reconcile/gql_definitions/fragments/aws_infra_management_account.py +5 -5
  141. reconcile/gql_definitions/fragments/aws_organization.py +33 -0
  142. reconcile/gql_definitions/fragments/aws_vpc.py +5 -5
  143. reconcile/gql_definitions/fragments/aws_vpc_request.py +12 -5
  144. reconcile/gql_definitions/fragments/container_image_mirror.py +5 -5
  145. reconcile/gql_definitions/fragments/deploy_resources.py +5 -5
  146. reconcile/gql_definitions/fragments/disable.py +5 -5
  147. reconcile/gql_definitions/fragments/email_service.py +5 -5
  148. reconcile/gql_definitions/fragments/email_user.py +5 -5
  149. reconcile/gql_definitions/fragments/jumphost_common_fields.py +5 -5
  150. reconcile/gql_definitions/fragments/membership_source.py +5 -5
  151. reconcile/gql_definitions/fragments/minimal_ocm_organization.py +5 -5
  152. reconcile/gql_definitions/fragments/oc_connection_cluster.py +5 -5
  153. reconcile/gql_definitions/fragments/ocm_environment.py +5 -5
  154. reconcile/gql_definitions/fragments/pipeline_provider_retention.py +5 -5
  155. reconcile/gql_definitions/fragments/prometheus_instance.py +5 -5
  156. reconcile/gql_definitions/fragments/resource_limits_requirements.py +5 -5
  157. reconcile/gql_definitions/fragments/resource_requests_requirements.py +5 -5
  158. reconcile/gql_definitions/fragments/resource_values.py +5 -5
  159. reconcile/gql_definitions/fragments/saas_slo_document.py +5 -5
  160. reconcile/gql_definitions/fragments/saas_target_namespace.py +5 -5
  161. reconcile/gql_definitions/fragments/serviceaccount_token.py +5 -5
  162. reconcile/gql_definitions/fragments/terraform_state.py +5 -5
  163. reconcile/gql_definitions/fragments/upgrade_policy.py +5 -5
  164. reconcile/gql_definitions/fragments/user.py +5 -5
  165. reconcile/gql_definitions/fragments/vault_secret.py +5 -5
  166. reconcile/gql_definitions/gcp/gcp_docker_repos.py +5 -5
  167. reconcile/gql_definitions/gcp/gcp_projects.py +5 -5
  168. reconcile/gql_definitions/gitlab_members/gitlab_instances.py +5 -5
  169. reconcile/gql_definitions/gitlab_members/permissions.py +5 -5
  170. reconcile/gql_definitions/glitchtip/glitchtip_instance.py +5 -5
  171. reconcile/gql_definitions/glitchtip/glitchtip_project.py +5 -5
  172. reconcile/gql_definitions/glitchtip_project_alerts/glitchtip_project.py +5 -5
  173. reconcile/gql_definitions/integrations/integrations.py +5 -5
  174. reconcile/gql_definitions/introspection.json +775 -136
  175. reconcile/gql_definitions/jenkins_configs/jenkins_configs.py +5 -5
  176. reconcile/gql_definitions/jenkins_configs/jenkins_instances.py +5 -5
  177. reconcile/gql_definitions/jira/jira_servers.py +5 -5
  178. reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +9 -5
  179. reconcile/gql_definitions/jumphosts/jumphosts.py +5 -5
  180. reconcile/gql_definitions/ldap_groups/roles.py +5 -5
  181. reconcile/gql_definitions/ldap_groups/settings.py +5 -5
  182. reconcile/gql_definitions/maintenance/maintenances.py +5 -5
  183. reconcile/gql_definitions/membershipsources/roles.py +5 -5
  184. reconcile/gql_definitions/ocm_labels/clusters.py +5 -5
  185. reconcile/gql_definitions/ocm_labels/organizations.py +5 -5
  186. reconcile/gql_definitions/openshift_cluster_bots/clusters.py +5 -5
  187. reconcile/gql_definitions/openshift_groups/managed_groups.py +5 -5
  188. reconcile/gql_definitions/openshift_groups/managed_roles.py +5 -5
  189. reconcile/gql_definitions/openshift_serviceaccount_tokens/tokens.py +5 -5
  190. reconcile/gql_definitions/quay_membership/quay_membership.py +5 -5
  191. reconcile/gql_definitions/rhcs/certs.py +25 -79
  192. reconcile/gql_definitions/rhcs/openshift_resource_rhcs_cert.py +43 -0
  193. reconcile/gql_definitions/rhidp/organizations.py +5 -5
  194. reconcile/gql_definitions/service_dependencies/jenkins_instance_fragment.py +5 -5
  195. reconcile/gql_definitions/service_dependencies/service_dependencies.py +5 -5
  196. reconcile/gql_definitions/sharding/aws_accounts.py +5 -5
  197. reconcile/gql_definitions/sharding/ocm_organization.py +5 -5
  198. reconcile/gql_definitions/skupper_network/site_controller_template.py +5 -5
  199. reconcile/gql_definitions/skupper_network/skupper_networks.py +5 -5
  200. reconcile/gql_definitions/slack_usergroups/clusters.py +5 -5
  201. reconcile/gql_definitions/slack_usergroups/permissions.py +5 -5
  202. reconcile/gql_definitions/slack_usergroups/users.py +5 -5
  203. reconcile/gql_definitions/slo_documents/slo_documents.py +5 -5
  204. reconcile/gql_definitions/status_board/status_board.py +5 -5
  205. reconcile/gql_definitions/statuspage/statuspages.py +5 -5
  206. reconcile/gql_definitions/templating/template_collection.py +5 -5
  207. reconcile/gql_definitions/templating/templates.py +5 -5
  208. reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py +5 -5
  209. reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py +5 -5
  210. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_accounts.py +5 -5
  211. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_resources.py +5 -5
  212. reconcile/gql_definitions/terraform_cloudflare_users/app_interface_setting_cloudflare_and_vault.py +5 -5
  213. reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.py +5 -5
  214. reconcile/gql_definitions/terraform_init/aws_accounts.py +19 -5
  215. reconcile/gql_definitions/terraform_repo/terraform_repo.py +5 -5
  216. reconcile/gql_definitions/terraform_resources/database_access_manager.py +5 -5
  217. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +37 -7
  218. reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +15 -5
  219. reconcile/gql_definitions/unleash_feature_toggles/feature_toggles.py +5 -5
  220. reconcile/gql_definitions/vault_instances/vault_instances.py +5 -5
  221. reconcile/gql_definitions/vault_policies/vault_policies.py +5 -5
  222. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator.py +8 -5
  223. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator_peered_cluster_fragment.py +6 -5
  224. reconcile/integrations_manager.py +3 -3
  225. reconcile/jenkins_worker_fleets.py +10 -8
  226. reconcile/jira_permissions_validator.py +237 -122
  227. reconcile/ldap_groups/integration.py +1 -1
  228. reconcile/ocm/types.py +35 -57
  229. reconcile/ocm_aws_infrastructure_access.py +1 -1
  230. reconcile/ocm_clusters.py +4 -4
  231. reconcile/ocm_labels/integration.py +3 -2
  232. reconcile/ocm_machine_pools.py +33 -27
  233. reconcile/openshift_base.py +113 -4
  234. reconcile/openshift_cluster_bots.py +1 -1
  235. reconcile/openshift_namespace_labels.py +1 -1
  236. reconcile/openshift_namespaces.py +96 -101
  237. reconcile/openshift_resources_base.py +6 -2
  238. reconcile/openshift_rhcs_certs.py +74 -37
  239. reconcile/openshift_rolebindings.py +7 -11
  240. reconcile/openshift_saas_deploy.py +4 -5
  241. reconcile/openshift_saas_deploy_change_tester.py +9 -7
  242. reconcile/openshift_saas_deploy_trigger_cleaner.py +3 -5
  243. reconcile/openshift_serviceaccount_tokens.py +2 -2
  244. reconcile/openshift_upgrade_watcher.py +4 -4
  245. reconcile/oum/labelset.py +5 -3
  246. reconcile/oum/models.py +1 -4
  247. reconcile/prometheus_rules_tester/integration.py +3 -3
  248. reconcile/quay_base.py +25 -6
  249. reconcile/quay_membership.py +55 -29
  250. reconcile/quay_mirror.py +1 -1
  251. reconcile/quay_mirror_org.py +6 -4
  252. reconcile/quay_permissions.py +81 -75
  253. reconcile/quay_repos.py +35 -37
  254. reconcile/queries.py +132 -1
  255. reconcile/rhidp/common.py +3 -5
  256. reconcile/rhidp/sso_client/base.py +16 -5
  257. reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py +1 -1
  258. reconcile/saas_auto_promotions_manager/subscriber.py +4 -3
  259. reconcile/skupper_network/integration.py +2 -2
  260. reconcile/slack_usergroups.py +35 -14
  261. reconcile/sql_query.py +1 -0
  262. reconcile/status_board.py +6 -6
  263. reconcile/statuspage/atlassian.py +7 -7
  264. reconcile/statuspage/integrations/maintenances.py +4 -3
  265. reconcile/statuspage/page.py +4 -9
  266. reconcile/statuspage/status.py +5 -8
  267. reconcile/templates/rosa-classic-cluster-creation.sh.j2 +1 -1
  268. reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +1 -1
  269. reconcile/templating/lib/rendering.py +3 -3
  270. reconcile/templating/renderer.py +2 -2
  271. reconcile/templating/validator.py +4 -4
  272. reconcile/terraform_aws_route53.py +7 -1
  273. reconcile/terraform_cloudflare_dns.py +3 -3
  274. reconcile/terraform_cloudflare_resources.py +5 -5
  275. reconcile/terraform_cloudflare_users.py +3 -2
  276. reconcile/terraform_init/integration.py +187 -23
  277. reconcile/terraform_repo.py +16 -12
  278. reconcile/terraform_resources.py +6 -6
  279. reconcile/terraform_tgw_attachments.py +27 -19
  280. reconcile/terraform_users.py +7 -0
  281. reconcile/terraform_vpc_peerings.py +14 -3
  282. reconcile/terraform_vpc_resources/integration.py +20 -8
  283. reconcile/terraform_vpc_resources/merge_request.py +12 -2
  284. reconcile/terraform_vpc_resources/merge_request_manager.py +43 -19
  285. reconcile/typed_queries/aws_account_tags.py +41 -0
  286. reconcile/typed_queries/cost_report/app_names.py +1 -1
  287. reconcile/typed_queries/cost_report/cost_namespaces.py +2 -2
  288. reconcile/typed_queries/saas_files.py +20 -15
  289. reconcile/typed_queries/status_board.py +2 -2
  290. reconcile/unleash_feature_toggles/integration.py +4 -2
  291. reconcile/utils/acs/base.py +6 -3
  292. reconcile/utils/acs/policies.py +2 -2
  293. reconcile/utils/aws_api.py +51 -20
  294. reconcile/utils/aws_api_typed/api.py +38 -9
  295. reconcile/utils/aws_api_typed/cloudformation.py +149 -0
  296. reconcile/utils/aws_api_typed/logs.py +73 -0
  297. reconcile/utils/aws_api_typed/organization.py +4 -2
  298. reconcile/utils/binary.py +7 -12
  299. reconcile/utils/datetime_util.py +67 -0
  300. reconcile/utils/deadmanssnitch_api.py +1 -1
  301. reconcile/utils/differ.py +2 -3
  302. reconcile/utils/early_exit_cache.py +11 -12
  303. reconcile/utils/environ.py +5 -0
  304. reconcile/utils/expiration.py +7 -3
  305. reconcile/utils/external_resource_spec.py +2 -0
  306. reconcile/utils/filtering.py +1 -1
  307. reconcile/utils/gitlab_api.py +19 -5
  308. reconcile/utils/glitchtip/client.py +6 -2
  309. reconcile/utils/glitchtip/models.py +25 -28
  310. reconcile/utils/gql.py +4 -7
  311. reconcile/utils/helpers.py +1 -1
  312. reconcile/utils/instrumented_wrappers.py +1 -1
  313. reconcile/utils/internal_groups/client.py +2 -2
  314. reconcile/utils/internal_groups/models.py +8 -17
  315. reconcile/utils/jinja2/utils.py +6 -101
  316. reconcile/utils/jira_client.py +82 -63
  317. reconcile/utils/jjb_client.py +26 -13
  318. reconcile/utils/jobcontroller/controller.py +2 -2
  319. reconcile/utils/jobcontroller/models.py +17 -1
  320. reconcile/utils/json.py +43 -1
  321. reconcile/utils/membershipsources/app_interface_resolver.py +4 -2
  322. reconcile/utils/membershipsources/models.py +16 -23
  323. reconcile/utils/membershipsources/resolver.py +4 -2
  324. reconcile/utils/merge_request_manager/merge_request_manager.py +4 -4
  325. reconcile/utils/merge_request_manager/parser.py +6 -6
  326. reconcile/utils/metrics.py +5 -5
  327. reconcile/utils/models.py +304 -82
  328. reconcile/utils/mr/app_interface_reporter.py +2 -2
  329. reconcile/utils/mr/notificator.py +3 -3
  330. reconcile/utils/mr/update_access_report_base.py +3 -4
  331. reconcile/utils/mr/user_maintenance.py +3 -2
  332. reconcile/utils/oc.py +252 -201
  333. reconcile/utils/oc_filters.py +3 -3
  334. reconcile/utils/ocm/addons.py +0 -1
  335. reconcile/utils/ocm/base.py +17 -20
  336. reconcile/utils/ocm/cluster_groups.py +1 -1
  337. reconcile/utils/ocm/identity_providers.py +2 -2
  338. reconcile/utils/ocm/labels.py +1 -1
  339. reconcile/utils/ocm/products.py +8 -8
  340. reconcile/utils/ocm/search_filters.py +3 -6
  341. reconcile/utils/ocm/service_log.py +4 -6
  342. reconcile/utils/ocm/sre_capability_labels.py +20 -13
  343. reconcile/utils/openshift_resource.py +8 -3
  344. reconcile/utils/pagerduty_api.py +10 -7
  345. reconcile/utils/promotion_state.py +6 -11
  346. reconcile/utils/quay_api.py +74 -87
  347. reconcile/utils/raw_github_api.py +1 -1
  348. reconcile/utils/rhcsv2_certs.py +86 -23
  349. reconcile/utils/rosa/session.py +16 -0
  350. reconcile/utils/runtime/integration.py +2 -3
  351. reconcile/utils/runtime/runner.py +2 -2
  352. reconcile/utils/saasherder/interfaces.py +13 -20
  353. reconcile/utils/saasherder/models.py +23 -20
  354. reconcile/utils/saasherder/saasherder.py +50 -27
  355. reconcile/utils/slack_api.py +2 -2
  356. reconcile/utils/sloth.py +171 -2
  357. reconcile/utils/structs.py +1 -1
  358. reconcile/utils/terraform_client.py +5 -4
  359. reconcile/utils/terrascript_aws_client.py +274 -124
  360. reconcile/utils/unleash/server.py +2 -8
  361. reconcile/utils/vault.py +5 -12
  362. reconcile/utils/vcs.py +8 -8
  363. reconcile/vault_replication.py +107 -42
  364. reconcile/vpc_peerings_validator.py +13 -0
  365. tools/app_interface_reporter.py +4 -4
  366. tools/cli_commands/cost_report/cost_management_api.py +3 -3
  367. tools/cli_commands/cost_report/view.py +7 -6
  368. tools/cli_commands/erv2.py +1 -1
  369. tools/qontract_cli.py +28 -17
  370. tools/template_validation.py +3 -1
  371. {qontract_reconcile-0.10.2.dev361.dist-info → qontract_reconcile-0.10.2.dev474.dist-info}/entry_points.txt +0 -0
@@ -7,11 +7,7 @@ from collections.abc import Iterable, Mapping
7
7
  from enum import Enum
8
8
  from typing import Any, Self
9
9
 
10
- from pydantic import (
11
- BaseModel,
12
- Field,
13
- root_validator,
14
- )
10
+ from pydantic import BaseModel, Field, SerializeAsAny, model_validator
15
11
 
16
12
  from reconcile import queries
17
13
  from reconcile.gql_definitions.common.clusters import (
@@ -22,6 +18,7 @@ from reconcile.gql_definitions.common.clusters import (
22
18
  from reconcile.typed_queries.clusters import get_clusters
23
19
  from reconcile.utils.differ import diff_mappings
24
20
  from reconcile.utils.disabled_integrations import integration_is_enabled
21
+ from reconcile.utils.json import json_dumps
25
22
  from reconcile.utils.ocm import (
26
23
  DEFAULT_OCM_MACHINE_POOL_ID,
27
24
  OCM,
@@ -61,7 +58,8 @@ class MachinePoolAutoscaling(AbstractAutoscaling):
61
58
  min_replicas: int
62
59
  max_replicas: int
63
60
 
64
- @root_validator()
61
+ @model_validator(mode="before")
62
+ @classmethod
65
63
  def max_greater_min(cls, field_values: Mapping[str, Any]) -> Mapping[str, Any]:
66
64
  min_replicas = field_values.get("min_replicas")
67
65
  max_replicas = field_values.get("max_replicas")
@@ -82,7 +80,8 @@ class NodePoolAutoscaling(AbstractAutoscaling):
82
80
  min_replica: int
83
81
  max_replica: int
84
82
 
85
- @root_validator()
83
+ @model_validator(mode="before")
84
+ @classmethod
86
85
  def max_greater_min(cls, field_values: Mapping[str, Any]) -> Mapping[str, Any]:
87
86
  min_replica = field_values.get("min_replica")
88
87
  max_replica = field_values.get("max_replica")
@@ -103,14 +102,15 @@ class AbstractPool(ABC, BaseModel):
103
102
  # Abstract class for machine pools, to be implemented by OSD/HyperShift classes
104
103
 
105
104
  id: str
106
- replicas: int | None
107
- taints: list[Mapping[str, str]] | None
108
- labels: Mapping[str, str] | None
105
+ replicas: int | None = None
106
+ taints: list[Mapping[str, str]] | None = None
107
+ labels: Mapping[str, str] | None = None
109
108
  cluster: str
110
109
  cluster_type: ClusterType = Field(..., exclude=True)
111
- autoscaling: AbstractAutoscaling | None
110
+ autoscaling: SerializeAsAny[AbstractAutoscaling] | None = None
112
111
 
113
- @root_validator()
112
+ @model_validator(mode="before")
113
+ @classmethod
114
114
  def validate_scaling(cls, field_values: Mapping[str, Any]) -> Mapping[str, Any]:
115
115
  if field_values.get("autoscaling") and field_values.get("replicas"):
116
116
  raise ValueError("autoscaling and replicas are mutually exclusive")
@@ -154,13 +154,13 @@ class MachinePool(AbstractPool):
154
154
  instance_type: str
155
155
 
156
156
  def delete(self, ocm: OCM) -> None:
157
- ocm.delete_machine_pool(self.cluster, self.dict(by_alias=True))
157
+ ocm.delete_machine_pool(self.cluster, self.model_dump(by_alias=True))
158
158
 
159
159
  def create(self, ocm: OCM) -> None:
160
- ocm.create_machine_pool(self.cluster, self.dict(by_alias=True))
160
+ ocm.create_machine_pool(self.cluster, self.model_dump(by_alias=True))
161
161
 
162
162
  def update(self, ocm: OCM) -> None:
163
- update_dict = self.dict(by_alias=True)
163
+ update_dict = self.model_dump(by_alias=True)
164
164
  # can not update instance_type
165
165
  del update_dict["instance_type"]
166
166
  if not update_dict["labels"]:
@@ -170,7 +170,10 @@ class MachinePool(AbstractPool):
170
170
  ocm.update_machine_pool(self.cluster, update_dict)
171
171
 
172
172
  def has_diff(self, pool: ClusterMachinePoolV1) -> bool:
173
- if self.taints != pool.taints or self.labels != pool.labels:
173
+ pool_taints = (
174
+ [p.model_dump(by_alias=True) for p in pool.taints] if pool.taints else None
175
+ )
176
+ if self.taints != pool_taints or self.labels != pool.labels:
174
177
  logging.warning(
175
178
  f"updating labels or taints for machine pool {pool.q_id} "
176
179
  f"will only be applied to new Nodes"
@@ -178,7 +181,7 @@ class MachinePool(AbstractPool):
178
181
 
179
182
  return (
180
183
  self.replicas != pool.replicas
181
- or self.taints != pool.taints
184
+ or self.taints != pool_taints
182
185
  or self.labels != pool.labels
183
186
  or self.instance_type != pool.instance_type
184
187
  or self._has_diff_autoscale(pool)
@@ -214,7 +217,7 @@ class MachinePool(AbstractPool):
214
217
  replicas=pool.replicas,
215
218
  autoscaling=autoscaling,
216
219
  instance_type=pool.instance_type,
217
- taints=[p.dict(by_alias=True) for p in pool.taints or []],
220
+ taints=[p.model_dump(by_alias=True) for p in pool.taints or []],
218
221
  labels=pool.labels,
219
222
  cluster=cluster,
220
223
  cluster_type=cluster_type,
@@ -232,14 +235,14 @@ class NodePool(AbstractPool):
232
235
  subnet: str | None
233
236
 
234
237
  def delete(self, ocm: OCM) -> None:
235
- ocm.delete_node_pool(self.cluster, self.dict(by_alias=True))
238
+ ocm.delete_node_pool(self.cluster, self.model_dump(by_alias=True))
236
239
 
237
240
  def create(self, ocm: OCM) -> None:
238
- spec = self.dict(by_alias=True)
241
+ spec = self.model_dump(by_alias=True)
239
242
  ocm.create_node_pool(self.cluster, spec)
240
243
 
241
244
  def update(self, ocm: OCM) -> None:
242
- update_dict = self.dict(by_alias=True)
245
+ update_dict = self.model_dump(by_alias=True)
243
246
  # can not update instance_type
244
247
  del update_dict["aws_node_pool"]
245
248
  # can not update subnet
@@ -251,7 +254,10 @@ class NodePool(AbstractPool):
251
254
  ocm.update_node_pool(self.cluster, update_dict)
252
255
 
253
256
  def has_diff(self, pool: ClusterMachinePoolV1) -> bool:
254
- if self.taints != pool.taints or self.labels != pool.labels:
257
+ pool_taints = (
258
+ [p.model_dump(by_alias=True) for p in pool.taints] if pool.taints else None
259
+ )
260
+ if self.taints != pool_taints or self.labels != pool.labels:
255
261
  logging.warning(
256
262
  f"updating labels or taints for node pool {pool.q_id} "
257
263
  f"will only be applied to new Nodes"
@@ -259,7 +265,7 @@ class NodePool(AbstractPool):
259
265
 
260
266
  return (
261
267
  self.replicas != pool.replicas
262
- or self.taints != pool.taints
268
+ or self.taints != pool_taints
263
269
  or self.labels != pool.labels
264
270
  or self.aws_node_pool.instance_type != pool.instance_type
265
271
  or self.subnet != pool.subnet
@@ -297,7 +303,7 @@ class NodePool(AbstractPool):
297
303
  aws_node_pool=AWSNodePool(
298
304
  instance_type=pool.instance_type,
299
305
  ),
300
- taints=[p.dict(by_alias=True) for p in pool.taints or []],
306
+ taints=[p.model_dump(by_alias=True) for p in pool.taints or []],
301
307
  labels=pool.labels,
302
308
  subnet=pool.subnet,
303
309
  cluster=cluster,
@@ -312,7 +318,7 @@ class PoolHandler(BaseModel):
312
318
  pool: AbstractPool
313
319
 
314
320
  def act(self, dry_run: bool, ocm: OCM) -> None:
315
- logging.info(f"{self.action} {self.pool.dict(by_alias=True)}")
321
+ logging.info(f"{self.action} {self.pool.model_dump(by_alias=True)}")
316
322
  if dry_run:
317
323
  return
318
324
 
@@ -469,7 +475,7 @@ def calculate_diff(
469
475
  if invalid_diff:
470
476
  errors.append(
471
477
  InvalidUpdateError(
472
- f"can not update {invalid_diff} for existing machine pool on cluster {cluster_name}, CURRENT: {diff_pair.current.json()}, DESIRED: {diff_pair.desired.json()}"
478
+ f"can not update {invalid_diff} for existing machine pool on cluster {cluster_name}, CURRENT: {json_dumps(diff_pair.current)}, DESIRED: {json_dumps(diff_pair.desired)}"
473
479
  )
474
480
  )
475
481
  else:
@@ -531,7 +537,7 @@ def run(dry_run: bool) -> None:
531
537
 
532
538
  settings = queries.get_app_interface_settings()
533
539
  cluster_like_objects = [
534
- cluster.dict(by_alias=True) for cluster in filtered_clusters
540
+ cluster.model_dump(by_alias=True) for cluster in filtered_clusters
535
541
  ]
536
542
  ocm_map = OCMMap(
537
543
  clusters=cluster_like_objects,
@@ -29,10 +29,14 @@ from reconcile.utils import (
29
29
  metrics,
30
30
  )
31
31
  from reconcile.utils.constants import DEFAULT_THREAD_POOL_SIZE
32
+ from reconcile.utils.differ import DiffPair
32
33
  from reconcile.utils.oc import (
34
+ POD_RECYCLE_SUPPORTED_OWNER_KINDS,
35
+ AmbiguousResourceTypeError,
33
36
  DeploymentFieldIsImmutableError,
34
37
  FieldIsImmutableError,
35
38
  InvalidValueApplyError,
39
+ KindNotFoundError,
36
40
  MayNotChangeOnceSetError,
37
41
  MetaDataAnnotationsTooLongApplyError,
38
42
  OC_Map,
@@ -60,6 +64,10 @@ AUTH_METHOD_USER_KEY = {
60
64
  "oidc": "org_username",
61
65
  "rhidp": "org_username",
62
66
  }
67
+ RECYCLE_POD_ANNOTATIONS = [
68
+ "kubectl.kubernetes.io/restartedAt",
69
+ "openshift.openshift.io/restartedAt",
70
+ ]
63
71
 
64
72
 
65
73
  class ValidationError(Exception):
@@ -128,6 +136,29 @@ class ClusterMap(Protocol):
128
136
  ) -> list[str]: ...
129
137
 
130
138
 
139
+ def validate_managed_resource_types(
140
+ oc: OCCli,
141
+ managed_resource_types: Iterable[str],
142
+ managed_resource_names: Iterable[Mapping[str, Any]],
143
+ cluster_scope_resource_validation: bool,
144
+ ) -> None:
145
+ """Validate the managed resource types."""
146
+ managed_resources = [
147
+ managed_resource_name["resource"]
148
+ for managed_resource_name in managed_resource_names
149
+ ]
150
+ for managed_resource_type in managed_resource_types:
151
+ # The k8s kind must be supported by the cluster
152
+ resource = oc.get_api_resource(managed_resource_type)
153
+
154
+ if cluster_scope_resource_validation and not resource.namespaced:
155
+ # cluster-scoped resources must be use managedResourceNames!
156
+ if managed_resource_type not in managed_resources:
157
+ raise ValidationError(
158
+ f"Cluster-scoped resource {managed_resource_type} must be managed by name only. Please use 'managedResourceNames' field to specify the names of the resources to manage."
159
+ )
160
+
161
+
131
162
  def init_specs_to_fetch(
132
163
  ri: ResourceInventory,
133
164
  oc_map: ClusterMap,
@@ -136,6 +167,7 @@ def init_specs_to_fetch(
136
167
  override_managed_types: Iterable[str] | None = None,
137
168
  managed_types_key: str = "managedResourceTypes",
138
169
  cluster_admin: bool = False,
170
+ cluster_scope_resource_validation: bool = False,
139
171
  ) -> list[StateSpec]:
140
172
  state_specs: list[StateSpec] = []
141
173
 
@@ -163,9 +195,27 @@ def init_specs_to_fetch(
163
195
  logging.log(level=ex.log_level, msg=ex.message)
164
196
  continue
165
197
 
198
+ managed_resource_names = namespace_info.get("managedResourceNames") or []
199
+ try:
200
+ validate_managed_resource_types(
201
+ oc,
202
+ managed_types,
203
+ managed_resource_names,
204
+ cluster_scope_resource_validation=cluster_scope_resource_validation,
205
+ )
206
+ except KindNotFoundError:
207
+ # We must allow kinds that are not supported by the cluster because:
208
+ # 1. We install CRD with an operator in the same MR
209
+ # 2. SAAS files initialize the namespace objects with managedResourceTypes from the SAAS file
210
+ # and we can't expect that all of those are valid for all clusters
211
+ pass
212
+ except (AmbiguousResourceTypeError, ValidationError) as e:
213
+ ri.register_error()
214
+ logging.error(f"[{cluster}/{namespace_info['name']}] {e}")
215
+ continue
216
+
166
217
  namespace = namespace_info["name"]
167
218
  # These may exit but have a value of None
168
- managed_resource_names = namespace_info.get("managedResourceNames") or []
169
219
  managed_resource_type_overrides = (
170
220
  namespace_info.get("managedResourceTypeOverrides") or []
171
221
  )
@@ -340,6 +390,7 @@ def fetch_current_state(
340
390
  cluster_admin: bool = False,
341
391
  caller: str | None = None,
342
392
  init_projects: bool = False,
393
+ cluster_scope_resource_validation: bool = False,
343
394
  ) -> tuple[ResourceInventory, OC_Map]:
344
395
  ri = ResourceInventory()
345
396
  settings = queries.get_app_interface_settings()
@@ -362,6 +413,7 @@ def fetch_current_state(
362
413
  clusters=clusters,
363
414
  override_managed_types=override_managed_types,
364
415
  cluster_admin=cluster_admin,
416
+ cluster_scope_resource_validation=cluster_scope_resource_validation,
365
417
  )
366
418
  threaded.run(
367
419
  populate_current_state,
@@ -542,7 +594,7 @@ def apply(
542
594
  oc.resize_pvcs(namespace, owned_pvc_names, desired_storage)
543
595
 
544
596
  if recycle_pods:
545
- oc.recycle_pods(dry_run, namespace, resource_type, resource)
597
+ oc.recycle_pods(dry_run, namespace, resource)
546
598
 
547
599
 
548
600
  def create(
@@ -786,10 +838,56 @@ def handle_identical_resources(
786
838
  return actions
787
839
 
788
840
 
841
+ def patch_desired_resource_for_recycle_annotations(
842
+ desired: OR,
843
+ current: OR,
844
+ ) -> OR:
845
+ """
846
+ Patch desired resource with recycle annotations to pod template from current resource.
847
+ This is to avoid full pods recycle when changes are not affecting pod template.
848
+ Note desired annotations can override current annotations.
849
+ For example, if desired resource has kubectl.kubernetes.io/restartedAt defined,
850
+ it will be used instead of current resource annotation.
851
+
852
+ Args:
853
+ desired: desired resource
854
+ current: current resource
855
+
856
+ Returns:
857
+ patched desired resource
858
+ """
859
+ if current.kind not in POD_RECYCLE_SUPPORTED_OWNER_KINDS:
860
+ return desired
861
+
862
+ current_annotations = (
863
+ current.body.get("spec", {})
864
+ .get("template", {})
865
+ .get("metadata", {})
866
+ .get("annotations")
867
+ or {}
868
+ )
869
+ patch_annotations = {
870
+ k: value
871
+ for k in RECYCLE_POD_ANNOTATIONS
872
+ if (value := current_annotations.get(k))
873
+ }
874
+ if patch_annotations:
875
+ desired_annotations = (
876
+ desired.body.setdefault("spec", {})
877
+ .setdefault("template", {})
878
+ .setdefault("metadata", {})
879
+ .setdefault("annotations", {})
880
+ )
881
+ desired.body["spec"]["template"]["metadata"]["annotations"] = (
882
+ patch_annotations | desired_annotations
883
+ )
884
+ return desired
885
+
886
+
789
887
  def handle_modified_resources(
790
888
  oc_map: ClusterMap,
791
889
  ri: ResourceInventory,
792
- modified_resources: Mapping[Any, Any],
890
+ modified_resources: Mapping[str, DiffPair[OR, OR]],
793
891
  cluster: str,
794
892
  namespace: str,
795
893
  resource_type: str,
@@ -985,6 +1083,12 @@ def _realize_resource_data_3way_diff(
985
1083
  if options.enable_deletion and options.override_enable_deletion is False:
986
1084
  options.enable_deletion = False
987
1085
 
1086
+ for k in data["current"].keys() & data["desired"].keys():
1087
+ patch_desired_resource_for_recycle_annotations(
1088
+ desired=data["desired"][k],
1089
+ current=data["current"][k],
1090
+ )
1091
+
988
1092
  diff_result = differ.diff_mappings(
989
1093
  data["current"], data["desired"], equal=three_way_diff_using_hash
990
1094
  )
@@ -1363,6 +1467,11 @@ class HasOpenShiftResources(Protocol):
1363
1467
  openshift_resources: list | None
1364
1468
 
1365
1469
 
1470
+ @runtime_checkable
1471
+ class HasOpenShiftResourcesRequired(Protocol):
1472
+ openshift_resources: list
1473
+
1474
+
1366
1475
  @runtime_checkable
1367
1476
  class HasOpenshiftServiceAccountTokens(Protocol):
1368
1477
  openshift_service_account_tokens: list | None
@@ -1371,7 +1480,7 @@ class HasOpenshiftServiceAccountTokens(Protocol):
1371
1480
  @runtime_checkable
1372
1481
  class HasSharedResourcesOpenShiftResources(Protocol):
1373
1482
  @property
1374
- def shared_resources(self) -> Sequence[HasOpenShiftResources] | None: ...
1483
+ def shared_resources(self) -> Sequence[HasOpenShiftResourcesRequired] | None: ...
1375
1484
 
1376
1485
 
1377
1486
  @runtime_checkable
@@ -301,7 +301,7 @@ def filter_clusters(clusters: list[ClusterV1]) -> list[ClusterV1]:
301
301
 
302
302
  def get_ocm_map(clusters: list[ClusterV1]) -> OCMMap:
303
303
  settings = queries.get_app_interface_settings()
304
- clusters_info = [c.dict(by_alias=True) for c in clusters]
304
+ clusters_info = [c.model_dump(by_alias=True) for c in clusters]
305
305
  return OCMMap(
306
306
  settings=settings,
307
307
  clusters=clusters_info,
@@ -229,7 +229,7 @@ def get_gql_namespaces_in_shard() -> list[NamespaceV1]:
229
229
  return [
230
230
  ns
231
231
  for ns in all_namespaces
232
- if not ob.is_namespace_deleted(ns.dict(by_alias=True))
232
+ if not ob.is_namespace_deleted(ns.model_dump(by_alias=True))
233
233
  and is_in_shard(f"{ns.cluster.name}/{ns.name}")
234
234
  ]
235
235
 
@@ -1,25 +1,27 @@
1
1
  import logging
2
- import sys
2
+ from collections import defaultdict
3
3
  from collections.abc import (
4
4
  Callable,
5
5
  Iterable,
6
- Mapping,
7
6
  Sequence,
8
7
  )
8
+ from dataclasses import dataclass
9
+ from enum import StrEnum
9
10
  from typing import Any
10
11
 
11
12
  from sretoolbox.utils import threaded
12
13
 
13
14
  import reconcile.openshift_base as ob
14
15
  from reconcile.gql_definitions.common.namespaces_minimal import NamespaceV1
15
- from reconcile.status import ExitCodes
16
16
  from reconcile.typed_queries.app_interface_vault_settings import (
17
17
  get_app_interface_vault_settings,
18
18
  )
19
19
  from reconcile.typed_queries.namespaces_minimal import get_namespaces_minimal
20
20
  from reconcile.utils.constants import DEFAULT_THREAD_POOL_SIZE
21
21
  from reconcile.utils.defer import defer
22
- from reconcile.utils.oc_filters import filter_namespaces_by_cluster_and_namespace
22
+ from reconcile.utils.oc_filters import (
23
+ filter_namespaces_by_cluster_and_namespace,
24
+ )
23
25
  from reconcile.utils.oc_map import (
24
26
  OCLogMsg,
25
27
  OCMap,
@@ -30,113 +32,111 @@ from reconcile.utils.sharding import is_in_shard
30
32
 
31
33
  QONTRACT_INTEGRATION = "openshift-namespaces"
32
34
 
33
- NS_STATE_PRESENT = "present"
34
- NS_STATE_ABSENT = "absent"
35
-
36
- NS_ACTION_CREATE = "create"
37
- NS_ACTION_DELETE = "delete"
38
35
 
36
+ class Action(StrEnum):
37
+ CREATE = "create"
38
+ DELETE = "delete"
39
39
 
40
- DUPLICATES_LOG_MSG = "Found multiple definitions for the namespace {key}"
41
40
 
41
+ @dataclass(frozen=True)
42
+ class DesiredState:
43
+ cluster: str
44
+ namespace: str
45
+ delete: bool
46
+ cluster_admin: bool
42
47
 
43
- def get_desired_state(namespaces: Iterable[NamespaceV1]) -> list[dict[str, str]]:
44
- desired_state: list[dict[str, str]] = []
45
- for ns in namespaces:
46
- state = NS_STATE_PRESENT
47
- if ns.delete:
48
- state = NS_STATE_ABSENT
49
48
 
50
- desired_state.append({
51
- "cluster": ns.cluster.name,
52
- "namespace": ns.name,
53
- "desired_state": state,
54
- })
49
+ class NamespaceDuplicateError(Exception):
50
+ pass
55
51
 
56
- return desired_state
57
52
 
53
+ def get_namespaces(
54
+ cluster_name: Sequence[str] | None,
55
+ namespace_name: Sequence[str] | None,
56
+ ) -> tuple[list[NamespaceV1], list[NamespaceDuplicateError]]:
57
+ all_namespaces = get_namespaces_minimal()
58
58
 
59
- def get_shard_namespaces(
60
- namespaces: Iterable[NamespaceV1],
61
- ) -> tuple[list[NamespaceV1], bool]:
62
- # Structure holding duplicates by namespace key
63
- duplicates: dict[str, list[NamespaceV1]] = {}
64
- # namespace filtered list without duplicates
65
- filtered_ns: dict[str, NamespaceV1] = {}
66
-
67
- err = False
68
- for ns in namespaces:
69
- key = f"{ns.cluster.name}/{ns.name}"
59
+ namespaces_by_shard_key = defaultdict(list)
70
60
 
61
+ for namespace in all_namespaces:
62
+ key = f"{namespace.cluster.name}/{namespace.name}"
71
63
  if is_in_shard(key):
72
- if key not in filtered_ns:
73
- filtered_ns[key] = ns
74
- else:
75
- # Duplicated NS
76
- dupe_list_by_key = duplicates.setdefault(key, [])
77
- dupe_list_by_key.append(ns)
78
-
79
- for key, dupe_list in duplicates.items():
80
- dupe_list.append(filtered_ns[key])
81
- delete_flags = (
82
- [ns.delete for ns in dupe_list_by_key] if dupe_list_by_key else []
83
- )
64
+ namespaces_by_shard_key[key].append(namespace)
84
65
 
85
- if len(set(delete_flags)) > 1:
86
- # If true only some definitions in list have the delete flag.
87
- # this case will generate an error
88
- err = True
89
- # Remove the namespace found from the filtered list
90
- del filtered_ns[key]
91
- logging.error(DUPLICATES_LOG_MSG.format(key=key))
66
+ managed_namespaces = []
67
+ duplicate_errors = []
68
+
69
+ for key, namespaces in namespaces_by_shard_key.items():
70
+ if len(namespaces) == 1:
71
+ namespace = namespaces[0]
72
+ if not namespace.managed_by_external:
73
+ managed_namespaces.append(namespace)
92
74
  else:
93
- # If all namespaces have the same delete option
94
- # The action will be performaed
95
- logging.debug(DUPLICATES_LOG_MSG.format(key=key))
75
+ msg = f"Found multiple definitions for the namespace {key}"
76
+ duplicate_errors.append(NamespaceDuplicateError(msg))
77
+ logging.error(msg)
96
78
 
97
- return list(filtered_ns.values()), err
79
+ namespaces = filter_namespaces_by_cluster_and_namespace(
80
+ namespaces=managed_namespaces,
81
+ cluster_names=cluster_name,
82
+ namespace_names=namespace_name,
83
+ )
98
84
 
85
+ return namespaces, duplicate_errors
99
86
 
100
- def manage_namespaces(spec: Mapping[str, str], oc_map: OCMap, dry_run: bool) -> None:
101
- cluster = spec["cluster"]
102
- namespace = spec["namespace"]
103
- desired_state = spec["desired_state"]
104
87
 
105
- oc = oc_map.get(cluster)
88
+ def build_desired_state(
89
+ namespaces: Iterable[NamespaceV1],
90
+ ) -> list[DesiredState]:
91
+ return [
92
+ DesiredState(
93
+ cluster=namespace.cluster.name,
94
+ namespace=namespace.name,
95
+ delete=namespace.delete or False,
96
+ cluster_admin=namespace.cluster_admin or False,
97
+ )
98
+ for namespace in namespaces
99
+ ]
100
+
101
+
102
+ def manage_namespace(
103
+ desired_state: DesiredState,
104
+ oc_map: OCMap,
105
+ dry_run: bool,
106
+ ) -> None:
107
+ namespace = desired_state.namespace
108
+
109
+ oc = oc_map.get(desired_state.cluster, privileged=desired_state.cluster_admin)
106
110
  if isinstance(oc, OCLogMsg):
107
111
  logging.log(level=oc.log_level, msg=oc.message)
108
- return None
112
+ return
109
113
 
110
- act = {NS_ACTION_CREATE: oc.new_project, NS_ACTION_DELETE: oc.delete_project}
114
+ current_delete = not oc.project_exists(namespace)
111
115
 
112
- exists = oc.project_exists(namespace)
113
- action = None
114
- if not exists and desired_state == NS_STATE_PRESENT:
115
- if namespace.startswith("openshift-"):
116
- raise ValueError('cannot request a project starting with "openshift-"')
117
- action = NS_ACTION_CREATE
118
- elif exists and desired_state == NS_STATE_ABSENT:
119
- action = NS_ACTION_DELETE
116
+ if desired_state.delete == current_delete:
117
+ return
120
118
 
121
- if action:
122
- logging.info([action, cluster, namespace])
123
- if not dry_run:
124
- act[action](namespace)
119
+ action = Action.DELETE if desired_state.delete else Action.CREATE
125
120
 
121
+ logging.info([str(action), desired_state.cluster, namespace])
122
+ if not dry_run:
123
+ match action:
124
+ case Action.CREATE:
125
+ oc.new_project(namespace)
126
+ case Action.DELETE:
127
+ oc.delete_project(namespace)
126
128
 
127
- def check_results(
128
- desired_state: Iterable[Mapping[str, str]], results: Iterable[Any]
129
- ) -> bool:
130
- err = False
129
+
130
+ def build_runtime_errors(
131
+ desired_state: Iterable[DesiredState],
132
+ results: Iterable[Any],
133
+ ) -> list[Exception]:
134
+ exceptions = []
131
135
  for s, e in zip(desired_state, results, strict=False):
132
136
  if isinstance(e, Exception):
133
- err = True
134
- msg = (
135
- f"cluster: {s['cluster']}, namespace: {s['namespace']}, "
136
- f"exception: {e!s}"
137
- )
138
- logging.error(msg)
139
- return err
137
+ e.add_note(f"cluster: {s.cluster}, namespace: {s.namespace}")
138
+ exceptions.append(e)
139
+ return exceptions
140
140
 
141
141
 
142
142
  @defer
@@ -149,15 +149,8 @@ def run(
149
149
  namespace_name: Sequence[str] | None = None,
150
150
  defer: Callable | None = None,
151
151
  ) -> None:
152
- all_namespaces = get_namespaces_minimal()
153
- shard_namespaces, duplicates = get_shard_namespaces(all_namespaces)
154
- namespaces = filter_namespaces_by_cluster_and_namespace(
155
- namespaces=shard_namespaces,
156
- cluster_names=cluster_name,
157
- namespace_names=namespace_name,
158
- )
159
-
160
- desired_state = get_desired_state(namespaces)
152
+ namespaces, duplicate_errors = get_namespaces(cluster_name, namespace_name)
153
+ desired_state = build_desired_state(namespaces)
161
154
 
162
155
  vault_settings = get_app_interface_vault_settings()
163
156
  secret_reader = create_secret_reader(use_vault=vault_settings.vault)
@@ -171,16 +164,17 @@ def run(
171
164
  thread_pool_size=thread_pool_size,
172
165
  init_projects=True,
173
166
  )
174
-
175
167
  if defer:
176
168
  defer(oc_map.cleanup)
177
169
 
178
170
  ob.publish_cluster_desired_metrics_from_state(
179
- desired_state, QONTRACT_INTEGRATION, "Namespace"
171
+ state=({"cluster": s.cluster} for s in desired_state),
172
+ integration=QONTRACT_INTEGRATION,
173
+ kind="Namespace",
180
174
  )
181
175
 
182
176
  results = threaded.run(
183
- manage_namespaces,
177
+ manage_namespace,
184
178
  desired_state,
185
179
  thread_pool_size,
186
180
  return_exceptions=True,
@@ -188,6 +182,7 @@ def run(
188
182
  oc_map=oc_map,
189
183
  )
190
184
 
191
- err = check_results(desired_state=desired_state, results=results)
192
- if err or duplicates:
193
- sys.exit(ExitCodes.ERROR)
185
+ runtime_errors = build_runtime_errors(desired_state, results)
186
+ errors = runtime_errors + duplicate_errors
187
+ if errors:
188
+ raise ExceptionGroup("Reconcile errors occurred", errors)