qontract-reconcile 0.10.2.dev310__py3-none-any.whl → 0.10.2.dev439__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.

Potentially problematic release.


This version of qontract-reconcile might be problematic. Click here for more details.

Files changed (400) hide show
  1. {qontract_reconcile-0.10.2.dev310.dist-info → qontract_reconcile-0.10.2.dev439.dist-info}/METADATA +13 -12
  2. {qontract_reconcile-0.10.2.dev310.dist-info → qontract_reconcile-0.10.2.dev439.dist-info}/RECORD +396 -391
  3. reconcile/acs_rbac.py +2 -2
  4. reconcile/aus/advanced_upgrade_service.py +18 -12
  5. reconcile/aus/base.py +134 -32
  6. reconcile/aus/cluster_version_data.py +15 -5
  7. reconcile/aus/models.py +3 -1
  8. reconcile/aus/ocm_addons_upgrade_scheduler_org.py +1 -0
  9. reconcile/aus/ocm_upgrade_scheduler.py +8 -1
  10. reconcile/aus/ocm_upgrade_scheduler_org.py +20 -5
  11. reconcile/aus/version_gates/sts_version_gate_handler.py +54 -1
  12. reconcile/automated_actions/config/integration.py +16 -4
  13. reconcile/aws_account_manager/integration.py +8 -8
  14. reconcile/aws_account_manager/reconciler.py +3 -3
  15. reconcile/aws_ami_cleanup/integration.py +8 -12
  16. reconcile/aws_ami_share.py +69 -62
  17. reconcile/aws_cloudwatch_log_retention/integration.py +155 -126
  18. reconcile/aws_ecr_image_pull_secrets.py +5 -5
  19. reconcile/aws_iam_keys.py +1 -0
  20. reconcile/aws_saml_idp/integration.py +12 -4
  21. reconcile/aws_saml_roles/integration.py +32 -25
  22. reconcile/aws_version_sync/integration.py +125 -84
  23. reconcile/change_owners/bundle.py +3 -3
  24. reconcile/change_owners/change_log_tracking.py +3 -2
  25. reconcile/change_owners/change_owners.py +1 -1
  26. reconcile/change_owners/diff.py +2 -4
  27. reconcile/checkpoint.py +12 -4
  28. reconcile/cli.py +111 -18
  29. reconcile/cluster_deployment_mapper.py +2 -3
  30. reconcile/dashdotdb_dora.py +5 -12
  31. reconcile/dashdotdb_slo.py +1 -1
  32. reconcile/database_access_manager.py +125 -121
  33. reconcile/deadmanssnitch.py +1 -5
  34. reconcile/dynatrace_token_provider/integration.py +1 -1
  35. reconcile/endpoints_discovery/integration.py +4 -1
  36. reconcile/endpoints_discovery/merge_request.py +1 -1
  37. reconcile/endpoints_discovery/merge_request_manager.py +9 -11
  38. reconcile/external_resources/factories.py +5 -12
  39. reconcile/external_resources/integration.py +1 -1
  40. reconcile/external_resources/manager.py +8 -5
  41. reconcile/external_resources/meta.py +0 -1
  42. reconcile/external_resources/metrics.py +1 -1
  43. reconcile/external_resources/model.py +20 -20
  44. reconcile/external_resources/reconciler.py +7 -4
  45. reconcile/external_resources/secrets_sync.py +10 -14
  46. reconcile/external_resources/state.py +26 -16
  47. reconcile/fleet_labeler/integration.py +1 -1
  48. reconcile/gabi_authorized_users.py +8 -5
  49. reconcile/gcp_image_mirror.py +2 -2
  50. reconcile/github_org.py +1 -1
  51. reconcile/github_owners.py +4 -0
  52. reconcile/gitlab_housekeeping.py +13 -15
  53. reconcile/gitlab_members.py +6 -12
  54. reconcile/gitlab_mr_sqs_consumer.py +2 -2
  55. reconcile/gitlab_owners.py +15 -11
  56. reconcile/gitlab_permissions.py +8 -12
  57. reconcile/glitchtip_project_alerts/integration.py +3 -1
  58. reconcile/gql_definitions/acs/acs_instances.py +10 -10
  59. reconcile/gql_definitions/acs/acs_policies.py +5 -5
  60. reconcile/gql_definitions/acs/acs_rbac.py +6 -6
  61. reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py +32 -32
  62. reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py +26 -26
  63. reconcile/gql_definitions/app_interface_metrics_exporter/onboarding_status.py +6 -7
  64. reconcile/gql_definitions/app_sre_tekton_access_revalidation/roles.py +5 -5
  65. reconcile/gql_definitions/app_sre_tekton_access_revalidation/users.py +5 -5
  66. reconcile/gql_definitions/automated_actions/instance.py +51 -12
  67. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +11 -11
  68. reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +20 -10
  69. reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +28 -68
  70. reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +20 -10
  71. reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +20 -10
  72. reconcile/gql_definitions/aws_saml_roles/roles.py +5 -5
  73. reconcile/gql_definitions/aws_version_sync/clusters.py +10 -10
  74. reconcile/gql_definitions/aws_version_sync/namespaces.py +5 -5
  75. reconcile/gql_definitions/change_owners/queries/change_types.py +5 -5
  76. reconcile/gql_definitions/change_owners/queries/self_service_roles.py +9 -9
  77. reconcile/gql_definitions/cluster_auth_rhidp/clusters.py +18 -18
  78. reconcile/gql_definitions/common/alerting_services_settings.py +9 -9
  79. reconcile/gql_definitions/common/app_code_component_repos.py +5 -5
  80. reconcile/gql_definitions/common/app_interface_custom_messages.py +5 -5
  81. reconcile/gql_definitions/common/app_interface_dms_settings.py +5 -5
  82. reconcile/gql_definitions/common/app_interface_repo_settings.py +5 -5
  83. reconcile/gql_definitions/common/app_interface_roles.py +120 -0
  84. reconcile/gql_definitions/common/app_interface_state_settings.py +10 -10
  85. reconcile/gql_definitions/common/app_interface_vault_settings.py +5 -5
  86. reconcile/gql_definitions/common/app_quay_repos_escalation_policies.py +5 -5
  87. reconcile/gql_definitions/common/apps.py +5 -5
  88. reconcile/gql_definitions/common/aws_vpc_requests.py +22 -9
  89. reconcile/gql_definitions/common/aws_vpcs.py +11 -11
  90. reconcile/gql_definitions/common/clusters.py +37 -35
  91. reconcile/gql_definitions/common/clusters_minimal.py +14 -14
  92. reconcile/gql_definitions/common/clusters_with_dms.py +6 -6
  93. reconcile/gql_definitions/common/clusters_with_peering.py +29 -30
  94. reconcile/gql_definitions/common/github_orgs.py +10 -10
  95. reconcile/gql_definitions/common/jira_settings.py +10 -10
  96. reconcile/gql_definitions/common/jiralert_settings.py +5 -5
  97. reconcile/gql_definitions/common/ldap_settings.py +5 -5
  98. reconcile/gql_definitions/common/namespaces.py +42 -44
  99. reconcile/gql_definitions/common/namespaces_minimal.py +15 -13
  100. reconcile/gql_definitions/common/ocm_env_telemeter.py +12 -12
  101. reconcile/gql_definitions/common/ocm_environments.py +19 -19
  102. reconcile/gql_definitions/common/pagerduty_instances.py +9 -9
  103. reconcile/gql_definitions/common/pgp_reencryption_settings.py +6 -6
  104. reconcile/gql_definitions/common/pipeline_providers.py +29 -29
  105. reconcile/gql_definitions/common/quay_instances.py +5 -5
  106. reconcile/gql_definitions/common/quay_orgs.py +5 -5
  107. reconcile/gql_definitions/common/reserved_networks.py +5 -5
  108. reconcile/gql_definitions/common/rhcs_provider_settings.py +5 -5
  109. reconcile/gql_definitions/common/saas_files.py +44 -44
  110. reconcile/gql_definitions/common/saas_target_namespaces.py +10 -10
  111. reconcile/gql_definitions/common/saasherder_settings.py +5 -5
  112. reconcile/gql_definitions/common/slack_workspaces.py +5 -5
  113. reconcile/gql_definitions/common/smtp_client_settings.py +19 -19
  114. reconcile/gql_definitions/common/state_aws_account.py +7 -8
  115. reconcile/gql_definitions/common/users.py +5 -5
  116. reconcile/gql_definitions/common/users_with_paths.py +5 -5
  117. reconcile/gql_definitions/cost_report/app_names.py +5 -5
  118. reconcile/gql_definitions/cost_report/cost_namespaces.py +5 -5
  119. reconcile/gql_definitions/cost_report/settings.py +9 -9
  120. reconcile/gql_definitions/dashdotdb_slo/slo_documents_query.py +43 -43
  121. reconcile/gql_definitions/dynatrace_token_provider/dynatrace_bootstrap_tokens.py +10 -10
  122. reconcile/gql_definitions/dynatrace_token_provider/token_specs.py +5 -5
  123. reconcile/gql_definitions/email_sender/apps.py +5 -5
  124. reconcile/gql_definitions/email_sender/emails.py +8 -8
  125. reconcile/gql_definitions/email_sender/users.py +6 -6
  126. reconcile/gql_definitions/endpoints_discovery/apps.py +10 -10
  127. reconcile/gql_definitions/external_resources/aws_accounts.py +9 -9
  128. reconcile/gql_definitions/external_resources/external_resources_modules.py +23 -23
  129. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +494 -410
  130. reconcile/gql_definitions/external_resources/external_resources_settings.py +28 -26
  131. reconcile/gql_definitions/external_resources/fragments/external_resources_module_overrides.py +5 -5
  132. reconcile/gql_definitions/fleet_labeler/fleet_labels.py +40 -40
  133. reconcile/gql_definitions/fragments/aus_organization.py +5 -5
  134. reconcile/gql_definitions/fragments/aws_account_common.py +7 -5
  135. reconcile/gql_definitions/fragments/aws_account_managed.py +5 -5
  136. reconcile/gql_definitions/fragments/aws_account_sso.py +5 -5
  137. reconcile/gql_definitions/fragments/aws_infra_management_account.py +5 -5
  138. reconcile/gql_definitions/fragments/{aws_vpc_request_subnet.py → aws_organization.py} +12 -8
  139. reconcile/gql_definitions/fragments/aws_vpc.py +5 -5
  140. reconcile/gql_definitions/fragments/aws_vpc_request.py +12 -5
  141. reconcile/gql_definitions/fragments/container_image_mirror.py +5 -5
  142. reconcile/gql_definitions/fragments/deploy_resources.py +5 -5
  143. reconcile/gql_definitions/fragments/disable.py +5 -5
  144. reconcile/gql_definitions/fragments/email_service.py +5 -5
  145. reconcile/gql_definitions/fragments/email_user.py +5 -5
  146. reconcile/gql_definitions/fragments/jumphost_common_fields.py +5 -5
  147. reconcile/gql_definitions/fragments/membership_source.py +5 -5
  148. reconcile/gql_definitions/fragments/minimal_ocm_organization.py +5 -5
  149. reconcile/gql_definitions/fragments/oc_connection_cluster.py +5 -5
  150. reconcile/gql_definitions/fragments/ocm_environment.py +5 -5
  151. reconcile/gql_definitions/fragments/pipeline_provider_retention.py +5 -5
  152. reconcile/gql_definitions/fragments/prometheus_instance.py +5 -5
  153. reconcile/gql_definitions/fragments/resource_limits_requirements.py +5 -5
  154. reconcile/gql_definitions/fragments/resource_requests_requirements.py +5 -5
  155. reconcile/gql_definitions/fragments/resource_values.py +5 -5
  156. reconcile/gql_definitions/fragments/saas_slo_document.py +5 -5
  157. reconcile/gql_definitions/fragments/saas_target_namespace.py +5 -5
  158. reconcile/gql_definitions/fragments/serviceaccount_token.py +5 -5
  159. reconcile/gql_definitions/fragments/terraform_state.py +5 -5
  160. reconcile/gql_definitions/fragments/upgrade_policy.py +5 -5
  161. reconcile/gql_definitions/fragments/user.py +5 -5
  162. reconcile/gql_definitions/fragments/vault_secret.py +5 -5
  163. reconcile/gql_definitions/gcp/gcp_docker_repos.py +9 -9
  164. reconcile/gql_definitions/gcp/gcp_projects.py +9 -9
  165. reconcile/gql_definitions/gitlab_members/gitlab_instances.py +9 -9
  166. reconcile/gql_definitions/gitlab_members/permissions.py +9 -9
  167. reconcile/gql_definitions/glitchtip/glitchtip_instance.py +9 -9
  168. reconcile/gql_definitions/glitchtip/glitchtip_project.py +11 -11
  169. reconcile/gql_definitions/glitchtip_project_alerts/glitchtip_project.py +9 -9
  170. reconcile/gql_definitions/integrations/integrations.py +48 -51
  171. reconcile/gql_definitions/introspection.json +3510 -1865
  172. reconcile/gql_definitions/jenkins_configs/jenkins_configs.py +11 -11
  173. reconcile/gql_definitions/jenkins_configs/jenkins_instances.py +10 -10
  174. reconcile/gql_definitions/jira/jira_servers.py +5 -5
  175. reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +14 -10
  176. reconcile/gql_definitions/jumphosts/jumphosts.py +13 -13
  177. reconcile/gql_definitions/ldap_groups/roles.py +5 -5
  178. reconcile/gql_definitions/ldap_groups/settings.py +9 -9
  179. reconcile/gql_definitions/maintenance/maintenances.py +5 -5
  180. reconcile/gql_definitions/membershipsources/roles.py +5 -5
  181. reconcile/gql_definitions/ocm_labels/clusters.py +18 -19
  182. reconcile/gql_definitions/ocm_labels/organizations.py +5 -5
  183. reconcile/gql_definitions/openshift_cluster_bots/clusters.py +22 -22
  184. reconcile/gql_definitions/openshift_groups/managed_groups.py +5 -5
  185. reconcile/gql_definitions/openshift_groups/managed_roles.py +6 -6
  186. reconcile/gql_definitions/openshift_serviceaccount_tokens/tokens.py +10 -10
  187. reconcile/gql_definitions/quay_membership/quay_membership.py +6 -6
  188. reconcile/gql_definitions/rhcs/certs.py +33 -87
  189. reconcile/gql_definitions/rhcs/openshift_resource_rhcs_cert.py +43 -0
  190. reconcile/gql_definitions/rhidp/organizations.py +18 -18
  191. reconcile/gql_definitions/service_dependencies/jenkins_instance_fragment.py +5 -5
  192. reconcile/gql_definitions/service_dependencies/service_dependencies.py +8 -8
  193. reconcile/gql_definitions/sharding/aws_accounts.py +10 -10
  194. reconcile/gql_definitions/sharding/ocm_organization.py +8 -8
  195. reconcile/gql_definitions/skupper_network/site_controller_template.py +5 -5
  196. reconcile/gql_definitions/skupper_network/skupper_networks.py +10 -10
  197. reconcile/gql_definitions/slack_usergroups/clusters.py +5 -5
  198. reconcile/gql_definitions/slack_usergroups/permissions.py +9 -9
  199. reconcile/gql_definitions/slack_usergroups/users.py +5 -5
  200. reconcile/gql_definitions/slo_documents/slo_documents.py +5 -5
  201. reconcile/gql_definitions/status_board/status_board.py +6 -7
  202. reconcile/gql_definitions/statuspage/statuspages.py +9 -9
  203. reconcile/gql_definitions/templating/template_collection.py +5 -5
  204. reconcile/gql_definitions/templating/templates.py +5 -5
  205. reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py +6 -6
  206. reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py +11 -11
  207. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_accounts.py +11 -11
  208. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_resources.py +20 -25
  209. reconcile/gql_definitions/terraform_cloudflare_users/app_interface_setting_cloudflare_and_vault.py +6 -6
  210. reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.py +12 -12
  211. reconcile/gql_definitions/terraform_init/aws_accounts.py +23 -9
  212. reconcile/gql_definitions/terraform_repo/terraform_repo.py +9 -9
  213. reconcile/gql_definitions/terraform_resources/database_access_manager.py +5 -5
  214. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +450 -402
  215. reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +23 -17
  216. reconcile/gql_definitions/unleash_feature_toggles/feature_toggles.py +9 -9
  217. reconcile/gql_definitions/vault_instances/vault_instances.py +61 -61
  218. reconcile/gql_definitions/vault_policies/vault_policies.py +11 -11
  219. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator.py +8 -8
  220. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator_peered_cluster_fragment.py +5 -5
  221. reconcile/integrations_manager.py +3 -3
  222. reconcile/jenkins_job_builder.py +1 -1
  223. reconcile/jenkins_worker_fleets.py +80 -11
  224. reconcile/jira_permissions_validator.py +237 -122
  225. reconcile/ldap_groups/integration.py +1 -1
  226. reconcile/ocm/types.py +35 -56
  227. reconcile/ocm_aws_infrastructure_access.py +1 -1
  228. reconcile/ocm_clusters.py +4 -4
  229. reconcile/ocm_labels/integration.py +3 -2
  230. reconcile/ocm_machine_pools.py +33 -27
  231. reconcile/openshift_base.py +122 -10
  232. reconcile/openshift_cluster_bots.py +5 -5
  233. reconcile/openshift_groups.py +5 -0
  234. reconcile/openshift_limitranges.py +1 -1
  235. reconcile/openshift_namespace_labels.py +1 -1
  236. reconcile/openshift_namespaces.py +97 -101
  237. reconcile/openshift_resources_base.py +10 -5
  238. reconcile/openshift_rhcs_certs.py +77 -40
  239. reconcile/openshift_rolebindings.py +230 -130
  240. reconcile/openshift_saas_deploy.py +6 -7
  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 +8 -7
  244. reconcile/openshift_tekton_resources.py +1 -1
  245. reconcile/openshift_upgrade_watcher.py +4 -4
  246. reconcile/openshift_users.py +5 -3
  247. reconcile/oum/labelset.py +5 -3
  248. reconcile/oum/models.py +1 -4
  249. reconcile/oum/providers.py +1 -1
  250. reconcile/prometheus_rules_tester/integration.py +4 -4
  251. reconcile/quay_mirror.py +1 -1
  252. reconcile/queries.py +131 -0
  253. reconcile/requests_sender.py +8 -3
  254. reconcile/resource_scraper.py +1 -5
  255. reconcile/rhidp/common.py +3 -5
  256. reconcile/rhidp/sso_client/base.py +19 -10
  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/sendgrid_teammates.py +20 -9
  260. reconcile/skupper_network/integration.py +2 -2
  261. reconcile/slack_usergroups.py +35 -14
  262. reconcile/sql_query.py +1 -0
  263. reconcile/status.py +2 -2
  264. reconcile/status_board.py +6 -6
  265. reconcile/statuspage/atlassian.py +7 -7
  266. reconcile/statuspage/integrations/maintenances.py +4 -3
  267. reconcile/statuspage/page.py +4 -9
  268. reconcile/statuspage/status.py +5 -8
  269. reconcile/templates/rosa-classic-cluster-creation.sh.j2 +5 -1
  270. reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +4 -1
  271. reconcile/templating/lib/merge_request_manager.py +2 -2
  272. reconcile/templating/lib/rendering.py +3 -3
  273. reconcile/templating/renderer.py +12 -13
  274. reconcile/terraform_aws_route53.py +18 -8
  275. reconcile/terraform_cloudflare_dns.py +3 -3
  276. reconcile/terraform_cloudflare_resources.py +12 -13
  277. reconcile/terraform_cloudflare_users.py +3 -2
  278. reconcile/terraform_init/integration.py +187 -23
  279. reconcile/terraform_repo.py +16 -12
  280. reconcile/terraform_resources.py +18 -10
  281. reconcile/terraform_tgw_attachments.py +28 -20
  282. reconcile/terraform_users.py +27 -22
  283. reconcile/terraform_vpc_peerings.py +15 -3
  284. reconcile/terraform_vpc_resources/integration.py +23 -8
  285. reconcile/typed_queries/app_interface_roles.py +10 -0
  286. reconcile/typed_queries/aws_account_tags.py +41 -0
  287. reconcile/typed_queries/cost_report/app_names.py +1 -1
  288. reconcile/typed_queries/cost_report/cost_namespaces.py +2 -2
  289. reconcile/typed_queries/saas_files.py +13 -13
  290. reconcile/typed_queries/status_board.py +2 -2
  291. reconcile/unleash_feature_toggles/integration.py +4 -2
  292. reconcile/utils/acs/base.py +6 -3
  293. reconcile/utils/acs/policies.py +2 -2
  294. reconcile/utils/aggregated_list.py +4 -3
  295. reconcile/utils/aws_api.py +51 -20
  296. reconcile/utils/aws_api_typed/api.py +38 -9
  297. reconcile/utils/aws_api_typed/cloudformation.py +149 -0
  298. reconcile/utils/aws_api_typed/logs.py +73 -0
  299. reconcile/utils/aws_api_typed/organization.py +4 -2
  300. reconcile/utils/binary.py +7 -12
  301. reconcile/utils/datetime_util.py +67 -0
  302. reconcile/utils/deadmanssnitch_api.py +1 -1
  303. reconcile/utils/differ.py +2 -3
  304. reconcile/utils/early_exit_cache.py +11 -12
  305. reconcile/utils/expiration.py +7 -3
  306. reconcile/utils/external_resource_spec.py +24 -1
  307. reconcile/utils/filtering.py +1 -1
  308. reconcile/utils/gitlab_api.py +7 -5
  309. reconcile/utils/glitchtip/client.py +6 -2
  310. reconcile/utils/glitchtip/models.py +25 -28
  311. reconcile/utils/gpg.py +5 -3
  312. reconcile/utils/gql.py +4 -7
  313. reconcile/utils/helm.py +2 -1
  314. reconcile/utils/helpers.py +1 -1
  315. reconcile/utils/imap_client.py +1 -1
  316. reconcile/utils/instrumented_wrappers.py +1 -1
  317. reconcile/utils/internal_groups/client.py +2 -2
  318. reconcile/utils/internal_groups/models.py +8 -17
  319. reconcile/utils/jenkins_api.py +24 -1
  320. reconcile/utils/jinja2/utils.py +6 -8
  321. reconcile/utils/jira_client.py +82 -63
  322. reconcile/utils/jjb_client.py +78 -46
  323. reconcile/utils/jobcontroller/controller.py +2 -2
  324. reconcile/utils/jobcontroller/models.py +17 -1
  325. reconcile/utils/json.py +74 -0
  326. reconcile/utils/ldap_client.py +4 -3
  327. reconcile/utils/lean_terraform_client.py +3 -1
  328. reconcile/utils/membershipsources/app_interface_resolver.py +4 -2
  329. reconcile/utils/membershipsources/models.py +16 -23
  330. reconcile/utils/membershipsources/resolver.py +4 -2
  331. reconcile/utils/merge_request_manager/merge_request_manager.py +4 -4
  332. reconcile/utils/merge_request_manager/parser.py +6 -6
  333. reconcile/utils/metrics.py +5 -5
  334. reconcile/utils/models.py +304 -82
  335. reconcile/utils/mr/__init__.py +3 -1
  336. reconcile/utils/mr/app_interface_reporter.py +6 -3
  337. reconcile/utils/mr/aws_access.py +1 -1
  338. reconcile/utils/mr/base.py +7 -13
  339. reconcile/utils/mr/clusters_updates.py +4 -2
  340. reconcile/utils/mr/notificator.py +3 -3
  341. reconcile/utils/mr/ocm_upgrade_scheduler_org_updates.py +4 -1
  342. reconcile/utils/mr/promote_qontract.py +28 -12
  343. reconcile/utils/mr/update_access_report_base.py +3 -4
  344. reconcile/utils/mr/user_maintenance.py +7 -6
  345. reconcile/utils/oc.py +445 -336
  346. reconcile/utils/oc_filters.py +3 -3
  347. reconcile/utils/ocm/addons.py +0 -1
  348. reconcile/utils/ocm/base.py +18 -21
  349. reconcile/utils/ocm/cluster_groups.py +1 -1
  350. reconcile/utils/ocm/identity_providers.py +2 -2
  351. reconcile/utils/ocm/labels.py +1 -1
  352. reconcile/utils/ocm/ocm.py +81 -71
  353. reconcile/utils/ocm/products.py +9 -3
  354. reconcile/utils/ocm/search_filters.py +3 -6
  355. reconcile/utils/ocm/service_log.py +4 -6
  356. reconcile/utils/ocm/sre_capability_labels.py +20 -13
  357. reconcile/utils/ocm_base_client.py +4 -4
  358. reconcile/utils/openshift_resource.py +83 -52
  359. reconcile/utils/openssl.py +2 -2
  360. reconcile/utils/output.py +3 -2
  361. reconcile/utils/pagerduty_api.py +10 -7
  362. reconcile/utils/promotion_state.py +6 -11
  363. reconcile/utils/raw_github_api.py +11 -8
  364. reconcile/utils/repo_owners.py +21 -29
  365. reconcile/utils/rhcsv2_certs.py +138 -35
  366. reconcile/utils/rosa/session.py +16 -0
  367. reconcile/utils/runtime/integration.py +2 -3
  368. reconcile/utils/runtime/meta.py +2 -1
  369. reconcile/utils/runtime/runner.py +2 -2
  370. reconcile/utils/saasherder/interfaces.py +13 -20
  371. reconcile/utils/saasherder/models.py +25 -21
  372. reconcile/utils/saasherder/saasherder.py +60 -32
  373. reconcile/utils/secret_reader.py +6 -6
  374. reconcile/utils/sharding.py +1 -1
  375. reconcile/utils/slack_api.py +26 -4
  376. reconcile/utils/sloth.py +224 -0
  377. reconcile/utils/sqs_gateway.py +16 -11
  378. reconcile/utils/state.py +2 -1
  379. reconcile/utils/structs.py +1 -1
  380. reconcile/utils/terraform_client.py +29 -26
  381. reconcile/utils/terrascript_aws_client.py +200 -116
  382. reconcile/utils/three_way_diff_strategy.py +1 -1
  383. reconcile/utils/unleash/server.py +2 -8
  384. reconcile/utils/vault.py +44 -41
  385. reconcile/utils/vcs.py +8 -8
  386. reconcile/vault_replication.py +119 -58
  387. tools/app_interface_reporter.py +4 -4
  388. tools/cli_commands/cost_report/cost_management_api.py +3 -3
  389. tools/cli_commands/cost_report/view.py +7 -6
  390. tools/cli_commands/erv2.py +1 -1
  391. tools/cli_commands/gpg_encrypt.py +4 -1
  392. tools/cli_commands/systems_and_tools.py +5 -1
  393. tools/qontract_cli.py +36 -21
  394. tools/template_validation.py +3 -1
  395. reconcile/gql_definitions/ocm_oidc_idp/__init__.py +0 -0
  396. reconcile/gql_definitions/ocm_subscription_labels/__init__.py +0 -0
  397. reconcile/jenkins/__init__.py +0 -0
  398. reconcile/jenkins/types.py +0 -77
  399. {qontract_reconcile-0.10.2.dev310.dist-info → qontract_reconcile-0.10.2.dev439.dist-info}/WHEEL +0 -0
  400. {qontract_reconcile-0.10.2.dev310.dist-info → qontract_reconcile-0.10.2.dev439.dist-info}/entry_points.txt +0 -0
reconcile/utils/oc.py CHANGED
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  import copy
4
+ import itertools
2
5
  import json
3
6
  import logging
4
7
  import os
@@ -7,20 +10,16 @@ import re
7
10
  import subprocess
8
11
  import threading
9
12
  import time
10
- from collections.abc import (
11
- Iterable,
12
- Mapping,
13
- )
13
+ from collections import defaultdict
14
14
  from contextlib import suppress
15
15
  from dataclasses import dataclass
16
- from datetime import datetime
17
16
  from functools import cache, wraps
18
17
  from subprocess import Popen
19
18
  from threading import Lock
20
- from typing import Any
19
+ from typing import TYPE_CHECKING, Any, TextIO, cast
21
20
 
22
21
  import urllib3
23
- from kubernetes.client import ( # type: ignore[attr-defined]
22
+ from kubernetes.client import (
24
23
  ApiClient,
25
24
  Configuration,
26
25
  )
@@ -48,12 +47,12 @@ from sretoolbox.utils import (
48
47
  )
49
48
 
50
49
  from reconcile.status import RunningState
50
+ from reconcile.utils.json import json_dumps
51
51
  from reconcile.utils.jump_host import (
52
52
  JumphostParameters,
53
53
  JumpHostSSH,
54
54
  )
55
55
  from reconcile.utils.metrics import reconcile_time
56
- from reconcile.utils.oc_connection_parameters import OCConnectionParameters
57
56
  from reconcile.utils.openshift_resource import OpenshiftResource as OR
58
57
  from reconcile.utils.secret_reader import (
59
58
  SecretNotFoundError,
@@ -61,10 +60,26 @@ from reconcile.utils.secret_reader import (
61
60
  )
62
61
  from reconcile.utils.unleash import get_feature_toggle_state
63
62
 
63
+ if TYPE_CHECKING:
64
+ from collections.abc import Callable, Iterable, Mapping, MutableMapping
65
+
66
+ from reconcile.utils.oc_connection_parameters import OCConnectionParameters
67
+
64
68
  urllib3.disable_warnings()
65
69
 
66
70
  GET_REPLICASET_MAX_ATTEMPTS = 20
67
-
71
+ DEFAULT_GROUP = ""
72
+ PROJECT_KIND = "Project.project.openshift.io"
73
+ POD_RECYCLE_SUPPORTED_TRIGGER_KINDS = [
74
+ "ConfigMap",
75
+ "Secret",
76
+ ]
77
+ POD_RECYCLE_SUPPORTED_OWNER_KINDS = [
78
+ "DaemonSet",
79
+ "Deployment",
80
+ "DeploymentConfig",
81
+ "StatefulSet",
82
+ ]
68
83
 
69
84
  oc_run_execution_counter = Counter(
70
85
  name="oc_run_execution_counter",
@@ -121,29 +136,29 @@ class JSONParsingError(Exception):
121
136
  pass
122
137
 
123
138
 
124
- class RecyclePodsUnsupportedKindError(Exception):
139
+ class PodNotReadyError(Exception):
125
140
  pass
126
141
 
127
142
 
128
- class RecyclePodsInvalidAnnotationValueError(Exception):
143
+ class JobNotRunningError(Exception):
129
144
  pass
130
145
 
131
146
 
132
- class PodNotReadyError(Exception):
147
+ class RequestEntityTooLargeError(Exception):
133
148
  pass
134
149
 
135
150
 
136
- class JobNotRunningError(Exception):
151
+ class KindNotFoundError(Exception):
137
152
  pass
138
153
 
139
154
 
140
- class RequestEntityTooLargeError(Exception):
155
+ class AmbiguousResourceTypeError(Exception):
141
156
  pass
142
157
 
143
158
 
144
159
  class OCDecorators:
145
160
  @classmethod
146
- def process_reconcile_time(cls, function):
161
+ def process_reconcile_time(cls, function: Callable) -> Callable:
147
162
  """
148
163
  Compare current time against bundle commit time and create log
149
164
  and metrics from it.
@@ -165,7 +180,7 @@ class OCDecorators:
165
180
  """
166
181
 
167
182
  @wraps(function)
168
- def wrapper(*args, **kwargs):
183
+ def wrapper(*args: Any, **kwargs: Any) -> list | tuple | Any:
169
184
  result = function(*args, **kwargs)
170
185
  msg = result[:-1] if isinstance(result, list | tuple) else result
171
186
 
@@ -173,6 +188,8 @@ class OCDecorators:
173
188
  return result
174
189
 
175
190
  running_state = RunningState()
191
+ if running_state.timestamp is None:
192
+ raise ValueError("Running state timestamp is None")
176
193
  commit_time = float(running_state.timestamp)
177
194
  time_spent = time.time() - commit_time
178
195
 
@@ -225,7 +242,9 @@ class OCProcessReconcileTimeDecoratorMsg:
225
242
  is_log_slow_oc_reconcile: bool
226
243
 
227
244
 
228
- def oc_process(template, parameters=None):
245
+ def oc_process(
246
+ template: Mapping[str, Any], parameters: Mapping[str, Any] | None = None
247
+ ) -> Iterable[dict[str, Any]]:
229
248
  oc = OCLocal(cluster_name="cluster", server=None, token=None, local=True)
230
249
  return oc.process(template, parameters)
231
250
 
@@ -252,7 +271,7 @@ class OCCliApiResource:
252
271
  namespaced: bool
253
272
 
254
273
  @property
255
- def group_version(self):
274
+ def group_version(self) -> str:
256
275
  if self.group:
257
276
  return f"{self.group}/{self.api_version}"
258
277
  return self.api_version
@@ -271,7 +290,7 @@ class OCCli:
271
290
  local: bool = False,
272
291
  insecure_skip_tls_verify: bool = False,
273
292
  connection_parameters: OCConnectionParameters | None = None,
274
- ):
293
+ ) -> None:
275
294
  """
276
295
  As of now we have to conform with 2 ways to initialize this client:
277
296
 
@@ -300,10 +319,10 @@ class OCCli:
300
319
  insecure_skip_tls_verify=insecure_skip_tls_verify,
301
320
  )
302
321
 
303
- def __enter__(self):
322
+ def __enter__(self) -> OCCli:
304
323
  return self
305
324
 
306
- def __exit__(self, *exc):
325
+ def __exit__(self, *exc: Any) -> None:
307
326
  self.cleanup()
308
327
 
309
328
  def _init_old_without_types(
@@ -317,7 +336,7 @@ class OCCli:
317
336
  init_api_resources: bool = False,
318
337
  local: bool = False,
319
338
  insecure_skip_tls_verify: bool = False,
320
- ):
339
+ ) -> None:
321
340
  """Initiates an OC client
322
341
 
323
342
  Args:
@@ -372,10 +391,7 @@ class OCCli:
372
391
 
373
392
  self.init_projects = init_projects
374
393
  if self.init_projects:
375
- if self.is_kind_supported("Project"):
376
- kind = "Project.project.openshift.io"
377
- else:
378
- kind = "Namespace"
394
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
379
395
  self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
380
396
 
381
397
  self.slow_oc_reconcile_threshold = float(
@@ -392,7 +408,7 @@ class OCCli:
392
408
  init_projects: bool = False,
393
409
  init_api_resources: bool = False,
394
410
  local: bool = False,
395
- ):
411
+ ) -> None:
396
412
  self.cluster_name = connection_parameters.cluster_name
397
413
  self.server = connection_parameters.server_url
398
414
  oc_base_cmd = ["oc", "--kubeconfig", "/dev/null"]
@@ -445,10 +461,7 @@ class OCCli:
445
461
 
446
462
  self.init_projects = init_projects
447
463
  if self.init_projects:
448
- if self.is_kind_supported("Project"):
449
- kind = "Project.project.openshift.io"
450
- else:
451
- kind = "Namespace"
464
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
452
465
  self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
453
466
 
454
467
  self.slow_oc_reconcile_threshold = float(
@@ -459,14 +472,14 @@ class OCCli:
459
472
  "LOG_SLOW_OC_RECONCILE", ""
460
473
  ).lower() in {"true", "yes"}
461
474
 
462
- def whoami(self):
475
+ def whoami(self) -> bytes:
463
476
  return self._run(["whoami"])
464
477
 
465
- def cleanup(self):
478
+ def cleanup(self) -> None:
466
479
  if hasattr(self, "jump_host") and isinstance(self.jump_host, JumpHostSSH):
467
480
  self.jump_host.cleanup()
468
481
 
469
- def get_items(self, kind, **kwargs):
482
+ def get_items(self, kind: str, **kwargs: Any) -> list[dict[str, Any]]:
470
483
  cmd = ["get", kind, "-o", "json"]
471
484
 
472
485
  if "namespace" in kwargs:
@@ -479,28 +492,33 @@ class OCCli:
479
492
  cmd.extend(["-n", namespace])
480
493
 
481
494
  if "labels" in kwargs:
482
- labels_list = [f"{k}={v}" for k, v in kwargs.get("labels").items()]
495
+ labels_list = [f"{k}={v}" for k, v in kwargs.get("labels", {}).items()]
483
496
  cmd += ["-l", ",".join(labels_list)]
484
497
 
485
498
  resource_names = kwargs.get("resource_names")
486
499
  if resource_names:
487
- items = []
500
+ resource_items = []
488
501
  for resource_name in resource_names:
489
502
  resource_cmd = cmd + [resource_name]
490
503
  item = self._run_json(resource_cmd, allow_not_found=True)
491
504
  if item:
492
- items.append(item)
493
- items_list = {"items": items}
505
+ resource_items.append(item)
506
+ items_list = {"items": resource_items}
494
507
  else:
495
508
  items_list = self._run_json(cmd)
496
509
 
497
510
  items = items_list.get("items")
498
511
  if items is None:
499
512
  raise Exception("Expecting items")
500
-
501
513
  return items
502
514
 
503
- def get(self, namespace, kind, name=None, allow_not_found=False):
515
+ def get(
516
+ self,
517
+ namespace: str | None,
518
+ kind: str,
519
+ name: str | None = None,
520
+ allow_not_found: bool = False,
521
+ ) -> dict[str, Any]:
504
522
  cmd = ["get", "-o", "json", kind]
505
523
  if name:
506
524
  cmd.append(name)
@@ -508,13 +526,15 @@ class OCCli:
508
526
  cmd.extend(["-n", namespace])
509
527
  return self._run_json(cmd, allow_not_found=allow_not_found)
510
528
 
511
- def get_all(self, kind, all_namespaces=False):
529
+ def get_all(self, kind: str, all_namespaces: bool = False) -> dict[str, Any]:
512
530
  cmd = ["get", "-o", "json", kind]
513
531
  if all_namespaces:
514
532
  cmd.append("--all-namespaces")
515
533
  return self._run_json(cmd)
516
534
 
517
- def remove_last_applied_configuration(self, namespace, kind, name):
535
+ def remove_last_applied_configuration(
536
+ self, namespace: str, kind: str, name: str
537
+ ) -> None:
518
538
  cmd = [
519
539
  "annotate",
520
540
  "-n",
@@ -525,7 +545,9 @@ class OCCli:
525
545
  ]
526
546
  self._run(cmd)
527
547
 
528
- def _msg_to_process_reconcile_time(self, namespace: str, resource: OR):
548
+ def _msg_to_process_reconcile_time(
549
+ self, namespace: str, resource: OR
550
+ ) -> OCProcessReconcileTimeDecoratorMsg:
529
551
  return OCProcessReconcileTimeDecoratorMsg(
530
552
  namespace=namespace,
531
553
  resource=resource,
@@ -534,7 +556,9 @@ class OCCli:
534
556
  is_log_slow_oc_reconcile=self.is_log_slow_oc_reconcile,
535
557
  )
536
558
 
537
- def process(self, template, parameters=None):
559
+ def process(
560
+ self, template: Mapping[str, Any], parameters: Mapping[str, Any] | None = None
561
+ ) -> Iterable[dict[str, Any]]:
538
562
  if parameters is None:
539
563
  parameters = {}
540
564
  parameters_to_process = [f"{k}={v}" for k, v in parameters.items()]
@@ -545,36 +569,44 @@ class OCCli:
545
569
  "-f",
546
570
  "-",
547
571
  ] + parameters_to_process
548
- result = self._run(cmd, stdin=json.dumps(template, sort_keys=True))
572
+ result = self._run(cmd, stdin=json_dumps(template))
549
573
  return json.loads(result)["items"]
550
574
 
551
575
  @OCDecorators.process_reconcile_time
552
- def apply(self, namespace, resource):
576
+ def apply(self, namespace: str, resource: OR) -> OCProcessReconcileTimeDecoratorMsg:
553
577
  cmd = ["apply", "-n", namespace, "-f", "-"]
554
578
  self._run(cmd, stdin=resource.to_json(), apply=True)
555
579
  return self._msg_to_process_reconcile_time(namespace, resource)
556
580
 
557
581
  @OCDecorators.process_reconcile_time
558
- def create(self, namespace, resource):
582
+ def create(
583
+ self, namespace: str, resource: OR
584
+ ) -> OCProcessReconcileTimeDecoratorMsg:
559
585
  cmd = ["create", "-n", namespace, "-f", "-"]
560
586
  self._run(cmd, stdin=resource.to_json(), apply=True)
561
587
  return self._msg_to_process_reconcile_time(namespace, resource)
562
588
 
563
589
  @OCDecorators.process_reconcile_time
564
- def replace(self, namespace, resource):
590
+ def replace(
591
+ self, namespace: str, resource: OR
592
+ ) -> OCProcessReconcileTimeDecoratorMsg:
565
593
  cmd = ["replace", "-n", namespace, "-f", "-"]
566
594
  self._run(cmd, stdin=resource.to_json(), apply=True)
567
595
  return self._msg_to_process_reconcile_time(namespace, resource)
568
596
 
569
597
  @OCDecorators.process_reconcile_time
570
- def patch(self, namespace, kind, name, patch):
571
- cmd = ["patch", "-n", namespace, kind, name, "-p", json.dumps(patch)]
598
+ def patch(
599
+ self, namespace: str, kind: str, name: str, patch: Mapping[str, Any]
600
+ ) -> OCProcessReconcileTimeDecoratorMsg:
601
+ cmd = ["patch", "-n", namespace, kind, name, "-p", json_dumps(patch)]
572
602
  self._run(cmd)
573
603
  resource = OR({"kind": kind, "metadata": {"name": name}}, "", "")
574
604
  return self._msg_to_process_reconcile_time(namespace, resource)
575
605
 
576
606
  @OCDecorators.process_reconcile_time
577
- def delete(self, namespace, kind, name, cascade=True):
607
+ def delete(
608
+ self, namespace: str, kind: str, name: str, cascade: bool = True
609
+ ) -> OCProcessReconcileTimeDecoratorMsg:
578
610
  cmd = [
579
611
  "delete",
580
612
  "-n",
@@ -589,7 +621,14 @@ class OCCli:
589
621
  return self._msg_to_process_reconcile_time(namespace, resource)
590
622
 
591
623
  @OCDecorators.process_reconcile_time
592
- def label(self, namespace, kind, name, labels, overwrite=False):
624
+ def label(
625
+ self,
626
+ namespace: str | None,
627
+ kind: str,
628
+ name: str,
629
+ labels: Mapping[str, str | None],
630
+ overwrite: bool = False,
631
+ ) -> OCProcessReconcileTimeDecoratorMsg:
593
632
  ns = ["-n", namespace] if namespace else []
594
633
  added = [f"{k}={v}" for k, v in labels.items() if v is not None]
595
634
  removed = [f"{k}-" for k, v in labels.items() if v is None]
@@ -598,16 +637,14 @@ class OCCli:
598
637
  cmd.extend(added + removed)
599
638
  self._run(cmd)
600
639
  resource = OR({"kind": kind, "metadata": {"name": name}}, "", "")
601
- return self._msg_to_process_reconcile_time(namespace, resource)
640
+ return self._msg_to_process_reconcile_time(namespace or "", resource)
602
641
 
603
- def project_exists(self, name):
642
+ def project_exists(self, name: str) -> bool:
604
643
  if name in self.projects:
605
644
  return True
645
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
606
646
  try:
607
- if self.is_kind_supported("Project"):
608
- self.get(None, "Project.project.openshift.io", name)
609
- else:
610
- self.get(None, "Namespace", name)
647
+ self.get(None, kind, name)
611
648
  except StatusCodeError as e:
612
649
  if "NotFound" in str(e):
613
650
  return False
@@ -615,8 +652,8 @@ class OCCli:
615
652
  return True
616
653
 
617
654
  @OCDecorators.process_reconcile_time
618
- def new_project(self, namespace):
619
- if self.is_kind_supported("Project"):
655
+ def new_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
656
+ if self.is_kind_supported(PROJECT_KIND):
620
657
  cmd = ["new-project", namespace]
621
658
  else:
622
659
  cmd = ["create", "namespace", namespace]
@@ -631,8 +668,8 @@ class OCCli:
631
668
  return self._msg_to_process_reconcile_time(namespace, resource)
632
669
 
633
670
  @OCDecorators.process_reconcile_time
634
- def delete_project(self, namespace):
635
- if self.is_kind_supported("Project"):
671
+ def delete_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
672
+ if self.is_kind_supported(PROJECT_KIND):
636
673
  cmd = ["delete", "project", namespace]
637
674
  else:
638
675
  cmd = ["delete", "namespace", namespace]
@@ -642,7 +679,7 @@ class OCCli:
642
679
  resource = OR({"kind": "Namespace", "metadata": {"name": namespace}}, "", "")
643
680
  return self._msg_to_process_reconcile_time(namespace, resource)
644
681
 
645
- def get_group_if_exists(self, name):
682
+ def get_group_if_exists(self, name: str) -> dict[str, Any] | None:
646
683
  try:
647
684
  return self.get(None, "Group", name)
648
685
  except StatusCodeError as e:
@@ -650,20 +687,20 @@ class OCCli:
650
687
  return None
651
688
  raise e
652
689
 
653
- def create_group(self, group):
690
+ def create_group(self, group: str) -> None:
654
691
  if self.get_group_if_exists(group) is not None:
655
692
  return
656
693
  cmd = ["adm", "groups", "new", group]
657
694
  self._run(cmd)
658
695
 
659
- def delete_group(self, group):
696
+ def delete_group(self, group: str) -> None:
660
697
  cmd = ["delete", "group", group]
661
698
  self._run(cmd)
662
699
 
663
- def get_users(self):
700
+ def get_users(self) -> Iterable[dict[str, Any]]:
664
701
  return self.get_all("User")["items"]
665
702
 
666
- def delete_user(self, user_name):
703
+ def delete_user(self, user_name: str) -> None:
667
704
  user = self.get(None, "User", user_name)
668
705
  cmd = ["delete", "user", user_name]
669
706
  self._run(cmd)
@@ -671,19 +708,19 @@ class OCCli:
671
708
  cmd = ["delete", "identity", identity]
672
709
  self._run(cmd)
673
710
 
674
- def add_user_to_group(self, group, user):
711
+ def add_user_to_group(self, group: str, user: str) -> None:
675
712
  cmd = ["adm", "groups", "add-users", group, user]
676
713
  self._run(cmd)
677
714
 
678
- def del_user_from_group(self, group, user):
715
+ def del_user_from_group(self, group: str, user: str) -> None:
679
716
  cmd = ["adm", "groups", "remove-users", group, user]
680
717
  self._run(cmd)
681
718
 
682
- def sa_get_token(self, namespace, name):
719
+ def sa_get_token(self, namespace: str, name: str) -> str:
683
720
  cmd = ["sa", "-n", namespace, "get-token", name]
684
- return self._run(cmd)
721
+ return self._run(cmd).decode("utf-8")
685
722
 
686
- def get_api_resources(self):
723
+ def get_api_resources(self) -> dict[str, list[OCCliApiResource]]:
687
724
  with self.api_resources_lock:
688
725
  if not self.api_resources:
689
726
  cmd = ["api-resources", "--no-headers"]
@@ -704,13 +741,13 @@ class OCCli:
704
741
 
705
742
  return self.api_resources
706
743
 
707
- def get_version(self):
744
+ def get_version(self) -> bytes:
708
745
  # this is actually a 10 second timeout, because: oc reasons
709
746
  cmd = ["version", "--request-timeout=5"]
710
747
  return self._run(cmd)
711
748
 
712
749
  @retry(exceptions=(JobNotRunningError), max_attempts=20)
713
- def wait_for_job_running(self, namespace, name):
750
+ def wait_for_job_running(self, namespace: str, name: str) -> None:
714
751
  logging.info("waiting for job to run: " + name)
715
752
  pods = self.get_items("Pod", namespace=namespace, labels={"job-name": name})
716
753
 
@@ -725,13 +762,13 @@ class OCCli:
725
762
 
726
763
  def job_logs(
727
764
  self,
728
- namespace,
729
- name,
730
- follow,
731
- output,
732
- wait_for_job_running=True,
733
- wait_for_logs_process=False,
734
- ):
765
+ namespace: str,
766
+ name: str,
767
+ follow: bool,
768
+ output: str | pathlib.Path | TextIO,
769
+ wait_for_job_running: bool = True,
770
+ wait_for_logs_process: bool = False,
771
+ ) -> None:
735
772
  if wait_for_job_running:
736
773
  self.wait_for_job_running(namespace, name)
737
774
 
@@ -739,6 +776,7 @@ class OCCli:
739
776
  if follow:
740
777
  cmd.append("-f")
741
778
 
779
+ output_file: TextIO
742
780
  if isinstance(output, str | pathlib.Path):
743
781
  output_file = open(os.path.join(output, name), "w", encoding="locale") # noqa: SIM115
744
782
  else:
@@ -779,12 +817,11 @@ class OCCli:
779
817
  return output_file_name
780
818
 
781
819
  @staticmethod
782
- def get_service_account_username(user):
783
- namespace = user.split("/")[0]
784
- name = user.split("/")[1]
820
+ def get_service_account_username(user: str) -> str:
821
+ namespace, name, _ = user.split("/", maxsplit=2)
785
822
  return f"system:serviceaccount:{namespace}:{name}"
786
823
 
787
- def get_owned_pods(self, namespace, resource):
824
+ def get_owned_pods(self, namespace: str, resource: OR) -> list[dict[str, Any]]:
788
825
  pods = self.get(namespace, "Pod")["items"]
789
826
  owned_pods = []
790
827
  for p in pods:
@@ -797,7 +834,9 @@ class OCCli:
797
834
 
798
835
  return owned_pods
799
836
 
800
- def get_owned_replicasets(self, namespace, resource: dict) -> list[dict]:
837
+ def get_owned_replicasets(
838
+ self, namespace: str, resource: Mapping[str, Any]
839
+ ) -> list[dict[str, Any]]:
801
840
  owned_replicasets = []
802
841
  for rs in self.get(namespace, "ReplicaSet")["items"]:
803
842
  owner = self.get_obj_root_owner(namespace, rs, allow_not_found=True)
@@ -814,8 +853,11 @@ class OCCli:
814
853
  max_attempts=GET_REPLICASET_MAX_ATTEMPTS,
815
854
  )
816
855
  def get_replicaset(
817
- self, namespace: str, deployment_resource: dict, allow_empty=False
818
- ) -> dict:
856
+ self,
857
+ namespace: str,
858
+ deployment_resource: Mapping[str, Any],
859
+ allow_empty: bool = False,
860
+ ) -> dict[str, Any]:
819
861
  """Get last active ReplicaSet for given Deployment.
820
862
 
821
863
  Implements similar logic like in kubectl describe deployment.
@@ -835,7 +877,7 @@ class OCCli:
835
877
  raise ResourceNotFoundError("No ReplicaSet found")
836
878
 
837
879
  @staticmethod
838
- def get_pod_owned_pvc_names(pods: Iterable[dict[str, dict]]) -> set[str]:
880
+ def get_pod_owned_pvc_names(pods: Iterable[Mapping[str, Any]]) -> set[str]:
839
881
  owned_pvc_names = set()
840
882
  for p in pods:
841
883
  vols = p["spec"].get("volumes")
@@ -849,25 +891,28 @@ class OCCli:
849
891
  return owned_pvc_names
850
892
 
851
893
  @staticmethod
852
- def get_storage(resource):
894
+ def get_storage(resource: Mapping[str, Any]) -> str | None:
853
895
  # resources with volumeClaimTemplates
854
896
  with suppress(KeyError, IndexError):
855
897
  vct = resource["spec"]["volumeClaimTemplates"][0]
856
898
  return vct["spec"]["resources"]["requests"]["storage"]
899
+ return None
857
900
 
858
- def resize_pvcs(self, namespace, pvc_names, size):
901
+ def resize_pvcs(self, namespace: str, pvc_names: Iterable[str], size: str) -> None:
859
902
  patch = {"spec": {"resources": {"requests": {"storage": size}}}}
860
903
  for p in pvc_names:
861
904
  self.patch(namespace, "PersistentVolumeClaim", p, patch)
862
905
 
863
- def recycle_orphan_pods(self, namespace, pods):
906
+ def recycle_orphan_pods(
907
+ self, namespace: str, pods: Iterable[Mapping[str, Any]]
908
+ ) -> None:
864
909
  for p in pods:
865
910
  name = p["metadata"]["name"]
866
911
  self.delete(namespace, "Pod", name)
867
912
  self.validate_pod_ready(namespace, name)
868
913
 
869
914
  @retry(max_attempts=20)
870
- def validate_pod_ready(self, namespace, name):
915
+ def validate_pod_ready(self, namespace: str, name: str) -> None:
871
916
  logging.info([
872
917
  self.validate_pod_ready.__name__,
873
918
  self.cluster_name,
@@ -879,108 +924,113 @@ class OCCli:
879
924
  if not status["ready"]:
880
925
  raise PodNotReadyError(name)
881
926
 
882
- def recycle_pods(self, dry_run, namespace, dep_kind, dep_resource):
883
- """recycles pods which are using the specified resources.
884
- will only act on Secrets containing the 'qontract.recycle' annotation.
885
- dry_run: simulate pods recycle.
886
- namespace: namespace in which dependant resource is applied.
887
- dep_kind: dependant resource kind. currently only supports Secret.
888
- dep_resource: dependant resource."""
889
-
890
- supported_kinds = ["Secret", "ConfigMap"]
891
- if dep_kind not in supported_kinds:
927
+ def _is_resource_supported_to_trigger_recycle(
928
+ self,
929
+ namespace: str,
930
+ resource: OR,
931
+ ) -> bool:
932
+ if resource.kind not in POD_RECYCLE_SUPPORTED_TRIGGER_KINDS:
892
933
  logging.debug([
893
934
  "skipping_pod_recycle_unsupported",
894
935
  self.cluster_name,
895
936
  namespace,
896
- dep_kind,
937
+ resource.kind,
938
+ resource.name,
897
939
  ])
898
- return
940
+ return False
899
941
 
900
- dep_annotations = dep_resource.body["metadata"].get("annotations", {})
901
942
  # Note, that annotations might have been set to None explicitly
902
- dep_annotations = dep_resource.body["metadata"].get("annotations") or {}
903
- qontract_recycle = dep_annotations.get("qontract.recycle")
904
- if qontract_recycle is True:
905
- raise RecyclePodsInvalidAnnotationValueError('should be "true"')
943
+ annotations = resource.body["metadata"].get("annotations") or {}
944
+ qontract_recycle = annotations.get("qontract.recycle")
906
945
  if qontract_recycle != "true":
907
946
  logging.debug([
908
947
  "skipping_pod_recycle_no_annotation",
909
948
  self.cluster_name,
910
949
  namespace,
911
- dep_kind,
950
+ resource.kind,
951
+ resource.name,
912
952
  ])
953
+ return False
954
+ return True
955
+
956
+ def recycle_pods(
957
+ self,
958
+ dry_run: bool,
959
+ namespace: str,
960
+ resource: OR,
961
+ ) -> None:
962
+ """
963
+ recycles pods which are using the specified resources.
964
+ will only act on Secret or ConfigMap containing the 'qontract.recycle' annotation.
965
+
966
+ Args:
967
+ dry_run (bool): if True, will only log the recycle action without executing it
968
+ namespace (str): namespace of the resource
969
+ resource (OR): resource object (Secret or ConfigMap) to check for pod usage
970
+ """
971
+
972
+ if not self._is_resource_supported_to_trigger_recycle(namespace, resource):
913
973
  return
914
974
 
915
- dep_name = dep_resource.name
916
975
  pods = self.get(namespace, "Pod")["items"]
917
-
918
- if dep_kind == "Secret":
919
- pods_to_recycle = [
920
- pod for pod in pods if self.secret_used_in_pod(dep_name, pod)
921
- ]
922
- elif dep_kind == "ConfigMap":
923
- pods_to_recycle = [
924
- pod for pod in pods if self.configmap_used_in_pod(dep_name, pod)
925
- ]
926
- else:
927
- raise RecyclePodsUnsupportedKindError(dep_kind)
928
-
929
- recyclables = {}
930
- supported_recyclables = [
931
- "Deployment",
932
- "DeploymentConfig",
933
- "StatefulSet",
934
- "DaemonSet",
976
+ pods_to_recycle = [
977
+ pod
978
+ for pod in pods
979
+ if self.is_resource_used_in_pod(
980
+ name=resource.name,
981
+ kind=resource.kind,
982
+ pod=pod,
983
+ )
935
984
  ]
985
+
986
+ recycle_names_by_kind = defaultdict(set)
936
987
  for pod in pods_to_recycle:
937
988
  owner = self.get_obj_root_owner(namespace, pod, allow_not_found=True)
938
989
  kind = owner["kind"]
939
- if kind not in supported_recyclables:
940
- continue
941
- recyclables.setdefault(kind, [])
942
- exists = False
943
- for obj in recyclables[kind]:
944
- owner_name = owner["metadata"]["name"]
945
- if obj["metadata"]["name"] == owner_name:
946
- exists = True
947
- break
948
- if not exists:
949
- recyclables[kind].append(owner)
950
-
951
- for kind, objs in recyclables.items():
952
- for obj in objs:
953
- self.recycle(dry_run, namespace, kind, obj)
954
-
955
- @retry(exceptions=ObjectHasBeenModifiedError)
956
- def recycle(self, dry_run, namespace, kind, obj):
957
- """Recycles an object by adding a recycle.time annotation
958
-
959
- :param dry_run: Is this a dry run
960
- :param namespace: Namespace to work in
961
- :param kind: Object kind
962
- :param obj: Object to recycle
990
+ if kind in POD_RECYCLE_SUPPORTED_OWNER_KINDS:
991
+ recycle_names_by_kind[kind].add(owner["metadata"]["name"])
992
+
993
+ for kind, names in recycle_names_by_kind.items():
994
+ for name in names:
995
+ self.recycle(
996
+ dry_run=dry_run,
997
+ namespace=namespace,
998
+ kind=kind,
999
+ name=name,
1000
+ )
1001
+
1002
+ def recycle(
1003
+ self,
1004
+ dry_run: bool,
1005
+ namespace: str,
1006
+ kind: str,
1007
+ name: str,
1008
+ ) -> None:
1009
+ """
1010
+ Recycles an object using oc rollout restart, which will add an annotation
1011
+ kubectl.kubernetes.io/restartedAt with the current timestamp to the pod
1012
+ template, triggering a rolling restart.
1013
+
1014
+ Args:
1015
+ dry_run (bool): if True, will only log the recycle action without executing it
1016
+ namespace (str): namespace of the object to recycle
1017
+ kind (str): kind of the object to recycle
1018
+ name (str): name of the object to recycle
963
1019
  """
964
- name = obj["metadata"]["name"]
965
1020
  logging.info([f"recycle_{kind.lower()}", self.cluster_name, namespace, name])
966
1021
  if not dry_run:
967
- now = datetime.now()
968
- recycle_time = now.strftime("%d/%m/%Y %H:%M:%S")
969
-
970
- # get the object in case it was modified
971
- obj = self.get(namespace, kind, name)
972
- # honor update strategy by setting annotations to force
973
- # a new rollout
974
- a = obj["spec"]["template"]["metadata"].get("annotations", {})
975
- a["recycle.time"] = recycle_time
976
- obj["spec"]["template"]["metadata"]["annotations"] = a
977
- cmd = ["apply", "-n", namespace, "-f", "-"]
978
- stdin = json.dumps(obj, sort_keys=True)
979
- self._run(cmd, stdin=stdin, apply=True)
1022
+ self._run(
1023
+ ["rollout", "restart", f"{kind}/{name}", "-n", namespace],
1024
+ apply=True,
1025
+ )
980
1026
 
981
1027
  def get_obj_root_owner(
982
- self, ns, obj, allow_not_found=False, allow_not_controller=False
983
- ):
1028
+ self,
1029
+ ns: str,
1030
+ obj: dict[str, Any],
1031
+ allow_not_found: bool = False,
1032
+ allow_not_controller: bool = False,
1033
+ ) -> dict[str, Any]:
984
1034
  """Get object root owner (recursively find the top level owner).
985
1035
  - Returns obj if it has no ownerReferences
986
1036
  - Returns obj if all ownerReferences have controller set to false
@@ -1014,39 +1064,65 @@ class OCCli:
1014
1064
  )
1015
1065
  return obj
1016
1066
 
1017
- def secret_used_in_pod(self, name, pod):
1018
- used_resources = self.get_resources_used_in_pod_spec(pod["spec"], "Secret")
1019
- return name in used_resources
1067
+ def is_resource_used_in_pod(
1068
+ self,
1069
+ name: str,
1070
+ kind: str,
1071
+ pod: Mapping[str, Any],
1072
+ ) -> bool:
1073
+ """
1074
+ Check if a resource (Secret or ConfigMap) is used in a Pod.
1020
1075
 
1021
- def configmap_used_in_pod(self, name, pod):
1022
- used_resources = self.get_resources_used_in_pod_spec(pod["spec"], "ConfigMap")
1076
+ Args:
1077
+ name: Name of the resource
1078
+ kind: "Secret" or "ConfigMap"
1079
+ pod: Pod object
1080
+
1081
+ Returns:
1082
+ True if the resource is used in the Pod, False otherwise.
1083
+ """
1084
+ used_resources = self.get_resources_used_in_pod_spec(pod["spec"], kind)
1023
1085
  return name in used_resources
1024
1086
 
1025
1087
  @staticmethod
1026
1088
  def get_resources_used_in_pod_spec(
1027
- spec: dict[str, Any],
1089
+ spec: Mapping[str, Any],
1028
1090
  kind: str,
1029
1091
  include_optional: bool = True,
1030
1092
  ) -> dict[str, set[str]]:
1031
- if kind not in {"Secret", "ConfigMap"}:
1032
- raise KeyError(f"unsupported resource kind: {kind}")
1093
+ """
1094
+ Get resources (Secrets or ConfigMaps) used in a Pod spec.
1095
+ Returns a dictionary where keys are resource names and values are sets of keys used from that resource.
1096
+
1097
+ Args:
1098
+ spec: Pod spec
1099
+ kind: "Secret" or "ConfigMap"
1100
+ include_optional: Whether to include optional resources
1101
+
1102
+ Returns:
1103
+ A dictionary mapping resource names to sets of keys used.
1104
+ """
1105
+ match kind:
1106
+ case "Secret":
1107
+ volume_kind, volume_kind_ref, env_from_kind, env_kind, env_ref = (
1108
+ "secret",
1109
+ "secretName",
1110
+ "secretRef",
1111
+ "secretKeyRef",
1112
+ "name",
1113
+ )
1114
+ case "ConfigMap":
1115
+ volume_kind, volume_kind_ref, env_from_kind, env_kind, env_ref = (
1116
+ "configMap",
1117
+ "name",
1118
+ "configMapRef",
1119
+ "configMapKeyRef",
1120
+ "name",
1121
+ )
1122
+ case _:
1123
+ raise KeyError(f"unsupported resource kind: {kind}")
1124
+
1033
1125
  optional = "optional"
1034
- if kind == "Secret":
1035
- volume_kind, volume_kind_ref, env_from_kind, env_kind, env_ref = (
1036
- "secret",
1037
- "secretName",
1038
- "secretRef",
1039
- "secretKeyRef",
1040
- "name",
1041
- )
1042
- elif kind == "ConfigMap":
1043
- volume_kind, volume_kind_ref, env_from_kind, env_kind, env_ref = (
1044
- "configMap",
1045
- "name",
1046
- "configMapRef",
1047
- "configMapKeyRef",
1048
- "name",
1049
- )
1050
1126
 
1051
1127
  resources: dict[str, set[str]] = {}
1052
1128
  for v in spec.get("volumes") or []:
@@ -1075,15 +1151,15 @@ class OCCli:
1075
1151
  continue
1076
1152
  resource_name = resource_ref[env_ref]
1077
1153
  resources.setdefault(resource_name, set())
1078
- secret_key = resource_ref["key"]
1079
- resources[resource_name].add(secret_key)
1154
+ key = resource_ref["key"]
1155
+ resources[resource_name].add(key)
1080
1156
  except (KeyError, TypeError):
1081
1157
  continue
1082
1158
 
1083
1159
  return resources
1084
1160
 
1085
1161
  @retry(exceptions=(StatusCodeError, NoOutputError), max_attempts=10)
1086
- def _run(self, cmd, **kwargs) -> bytes:
1162
+ def _run(self, cmd: list[str], **kwargs: Any) -> bytes:
1087
1163
  oc_run_execution_counter.labels(integration=RunningState().integration).inc()
1088
1164
  stdin = kwargs.get("stdin")
1089
1165
  stdin_text = stdin.encode() if stdin else None
@@ -1134,8 +1210,10 @@ class OCCli:
1134
1210
 
1135
1211
  return result.stdout.strip()
1136
1212
 
1137
- def _run_json(self, cmd, allow_not_found=False):
1138
- out = self._run(cmd, allow_not_found=allow_not_found)
1213
+ def _run_json(
1214
+ self, cmd: list[str], allow_not_found: bool = False
1215
+ ) -> dict[str, Any]:
1216
+ out = self._run(cmd, allow_not_found=allow_not_found).decode("utf-8")
1139
1217
 
1140
1218
  try:
1141
1219
  out_json = json.loads(out)
@@ -1144,76 +1222,90 @@ class OCCli:
1144
1222
 
1145
1223
  return out_json
1146
1224
 
1147
- def _parse_kind(self, kind_name):
1148
- # This is a provisional solution while we work in redefining
1149
- # the api resources initialization.
1150
- if not self.api_resources:
1151
- self.get_api_resources()
1225
+ def parse_kind(self, kind: str) -> tuple[str, str, str]:
1226
+ """Parse a Kubernetes kind string into its components.
1152
1227
 
1153
- kind_group = kind_name.split(".", 1)
1154
- kind = kind_group[0]
1155
- if kind in self.api_resources:
1156
- group_version = self.api_resources[kind][0].group_version
1157
- else:
1158
- raise StatusCodeError(f"{self.server}: {kind} does not exist")
1159
-
1160
- # if a kind_group has more than 1 entry than the kind_name is in
1161
- # the format kind.apigroup. Find the apigroup/version that matches
1162
- # the apigroup passed with the kind_name
1163
- if len(kind_group) > 1:
1164
- apigroup_override = kind_group[1]
1165
- find = False
1166
- for gv in self.api_resources[kind]:
1167
- if apigroup_override == gv.group:
1168
- if not gv.group:
1169
- group_version = gv.api_version
1170
- else:
1171
- group_version = f"{gv.group}/{gv.api_version}"
1172
- find = True
1173
- break
1174
-
1175
- if not find:
1176
- raise StatusCodeError(
1177
- f"{self.server}: {apigroup_override} does not have kind {kind}"
1178
- )
1179
- return (kind, group_version)
1228
+ Supports three formats:
1229
+ - kind
1230
+ - kind.group.whatever
1231
+ - kind.group.whatever/version
1232
+
1233
+ Args:
1234
+ kind: A Kubernetes kind string in one of the supported formats
1235
+
1236
+ Returns:
1237
+ Tuple of (kind, group, version) where missing parts are empty strings
1238
+
1239
+ Raises:
1240
+ ValueError: If the kind string format is invalid
1241
+
1242
+ Examples:
1243
+ >>> parse_kind_string("Deployment")
1244
+ ('Deployment', '', '')
1245
+ >>> parse_kind_string("ClusterRoleBinding.rbac.authorization.k8s.io")
1246
+ ('ClusterRoleBinding', 'rbac.authorization.k8s.io', '')
1247
+ >>> parse_kind_string("CustomResource.mygroup.example.com/v1")
1248
+ ('CustomResource', 'mygroup.example.com', 'v1')
1249
+ """
1250
+ pattern = r"^(?P<kind>[^./]+)(?:\.(?P<group>[^/]+))?(?:/(?P<version>.+))?$"
1251
+ match = re.match(pattern, kind)
1252
+ if not match:
1253
+ raise ValueError(f"Invalid kind string: {kind}")
1254
+
1255
+ kind = match.group("kind") or ""
1256
+ group = match.group("group") or DEFAULT_GROUP
1257
+ version = match.group("version") or ""
1258
+
1259
+ return kind, group, version
1180
1260
 
1181
1261
  def is_kind_supported(self, kind: str) -> bool:
1182
- # This is a provisional solution while we work in redefining
1183
- # the api resources initialization.
1184
- if not self.api_resources:
1185
- self.get_api_resources()
1262
+ """Returns True if the given kind is supported by the cluster, False otherwise.
1186
1263
 
1187
- if "." in kind:
1188
- try:
1189
- self._parse_kind(kind)
1190
- return True
1191
- except StatusCodeError:
1192
- return False
1193
- else:
1194
- return kind in self.api_resources
1264
+ Kind can be either kind, kind.group or kind.group/version."""
1265
+ try:
1266
+ self.get_api_resource(kind)
1267
+ return True
1268
+ except KindNotFoundError:
1269
+ return False
1195
1270
 
1196
1271
  def is_kind_namespaced(self, kind: str) -> bool:
1197
- # This is a provisional solution while we work in redefining
1198
- # the api resources initialization.
1272
+ """Returns True if the given kind is namespaced, False if it's cluster scoped.
1273
+
1274
+ Kind can be either kind, kind.group or kind.group/version."""
1275
+ return self.get_api_resource(kind).namespaced
1276
+
1277
+ def get_api_resource(self, kind: str) -> OCCliApiResource:
1278
+ """Return the OCCliApiResource for the given resource type.
1279
+
1280
+ Resource type can be either kind, kind.group or kind.group/version.
1281
+ If kind is not unique, group must be specified."""
1282
+
1199
1283
  if not self.api_resources:
1200
- self.get_api_resources()
1284
+ raise RuntimeError("API resources not initialized")
1201
1285
 
1202
- kg = kind.split(".", 1)
1203
- kind = kg[0]
1286
+ kind, group, _ = self.parse_kind(kind)
1204
1287
 
1205
- # Same Kinds might exist in different api groups
1206
- kind_resources = self.api_resources.get(kind)
1207
- if not kind_resources:
1208
- raise StatusCodeError(f"Kind {kind} does not exist in the ApiServer")
1288
+ if not (resources := self.api_resources.get(kind)):
1289
+ # the kind not found at all
1290
+ raise KindNotFoundError(f"Unsupported resource type: {kind}")
1291
+
1292
+ if len(resources) == 1 and group == DEFAULT_GROUP:
1293
+ return resources[0]
1294
+
1295
+ # get the resource with the specified group
1296
+ if resource := next((r for r in resources if r.group == group), None):
1297
+ return resource
1298
+
1299
+ # no resource with the specified group found
1300
+ if group == DEFAULT_GROUP:
1301
+ message = (
1302
+ f"Ambiguous resource type: {kind}. "
1303
+ "Please fully qualify it with its API group. E.g., ClusterRoleBinding -> ClusterRoleBinding.rbac.authorization.k8s.io"
1304
+ )
1305
+ raise AmbiguousResourceTypeError(message)
1209
1306
 
1210
- if len(kg) > 1:
1211
- group = kg[1]
1212
- for r in kind_resources:
1213
- if group == r.group:
1214
- return r.namespaced
1215
- raise StatusCodeError(f"Kind: {kind} does nod exist in the ApiServer")
1216
- return kind_resources[0].namespaced
1307
+ # group was specified but no matching resource found
1308
+ raise KindNotFoundError(f"Unsupported resource type: {kind}")
1217
1309
 
1218
1310
 
1219
1311
  REQUEST_TIMEOUT = 60
@@ -1231,7 +1323,7 @@ class OCNative(OCCli):
1231
1323
  local: bool = False,
1232
1324
  insecure_skip_tls_verify: bool = False,
1233
1325
  connection_parameters: OCConnectionParameters | None = None,
1234
- ):
1326
+ ) -> None:
1235
1327
  super().__init__(
1236
1328
  cluster_name,
1237
1329
  server,
@@ -1253,35 +1345,34 @@ class OCNative(OCCli):
1253
1345
 
1254
1346
  server = connection_parameters.server_url
1255
1347
 
1256
- if server:
1257
- self.client = self._get_client(server, token)
1258
- self.api_resources = self.get_api_resources()
1348
+ if not server:
1349
+ raise Exception("Server name is required!")
1259
1350
 
1260
- else:
1261
- raise Exception("A method relies on client/api_kind_version to be set")
1351
+ if not token:
1352
+ raise Exception("Token is required!")
1353
+
1354
+ self.client = self._get_client(server, token)
1355
+ self.api_resources = self.get_api_resources()
1262
1356
 
1263
1357
  self.projects = set()
1264
1358
  self.init_projects = init_projects
1265
1359
  if self.init_projects:
1266
- if self.is_kind_supported("Project"):
1267
- kind = "Project.project.openshift.io"
1268
- else:
1269
- kind = "Namespace"
1360
+ kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
1270
1361
  self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
1271
1362
 
1272
- def __enter__(self):
1363
+ def __enter__(self) -> OCNative:
1273
1364
  return self
1274
1365
 
1275
- def __exit__(self, *exc):
1366
+ def __exit__(self, *exc: Any) -> None:
1276
1367
  self.cleanup()
1277
1368
 
1278
- def cleanup(self):
1369
+ def cleanup(self) -> None:
1279
1370
  super().cleanup()
1280
1371
  if hasattr(self, "client") and self.client is not None:
1281
1372
  self.client.client.close()
1282
1373
 
1283
1374
  @retry(exceptions=(ServerTimeoutError, InternalServerError, ForbiddenError))
1284
- def _get_client(self, server, token):
1375
+ def _get_client(self, server: str, token: str) -> DynamicClient:
1285
1376
  opts = {
1286
1377
  "api_key": {"authorization": f"Bearer {token}"},
1287
1378
  "host": server,
@@ -1315,9 +1406,11 @@ class OCNative(OCCli):
1315
1406
  return self.client.resources.get(api_version=group_version, kind=kind)
1316
1407
 
1317
1408
  @retry(max_attempts=5, exceptions=(ServerTimeoutError))
1318
- def get_items(self, kind, **kwargs):
1319
- k, group_version = self._parse_kind(kind)
1320
- obj_client = self._get_obj_client(group_version=group_version, kind=k)
1409
+ def get_items(self, kind: str, **kwargs: Any) -> list[dict[str, Any]]:
1410
+ resource = self.get_api_resource(kind)
1411
+ obj_client = self._get_obj_client(
1412
+ group_version=resource.group_version, kind=resource.kind
1413
+ )
1321
1414
 
1322
1415
  namespace = ""
1323
1416
  if "namespace" in kwargs:
@@ -1330,13 +1423,12 @@ class OCNative(OCCli):
1330
1423
 
1331
1424
  labels = ""
1332
1425
  if "labels" in kwargs:
1333
- labels_list = [f"{k}={v}" for k, v in kwargs.get("labels").items()]
1334
-
1426
+ labels_list = [f"{k}={v}" for k, v in kwargs.get("labels", {}).items()]
1335
1427
  labels = ",".join(labels_list)
1336
1428
 
1337
1429
  resource_names = kwargs.get("resource_names")
1338
1430
  if resource_names:
1339
- items = []
1431
+ resource_items = []
1340
1432
  for resource_name in resource_names:
1341
1433
  try:
1342
1434
  item = obj_client.get(
@@ -1346,10 +1438,10 @@ class OCNative(OCCli):
1346
1438
  _request_timeout=REQUEST_TIMEOUT,
1347
1439
  )
1348
1440
  if item:
1349
- items.append(item.to_dict())
1441
+ resource_items.append(item.to_dict())
1350
1442
  except NotFoundError:
1351
1443
  pass
1352
- items_list = {"items": items}
1444
+ items_list = {"items": resource_items}
1353
1445
  else:
1354
1446
  items_list = obj_client.get(
1355
1447
  namespace=namespace,
@@ -1363,9 +1455,17 @@ class OCNative(OCCli):
1363
1455
  return items
1364
1456
 
1365
1457
  @retry(max_attempts=5, exceptions=(ServerTimeoutError, ForbiddenError))
1366
- def get(self, namespace, kind, name=None, allow_not_found=False):
1367
- k, group_version = self._parse_kind(kind)
1368
- obj_client = self._get_obj_client(group_version=group_version, kind=k)
1458
+ def get(
1459
+ self,
1460
+ namespace: str | None,
1461
+ kind: str,
1462
+ name: str | None = None,
1463
+ allow_not_found: bool = False,
1464
+ ) -> dict[str, Any]:
1465
+ resource = self.get_api_resource(kind)
1466
+ obj_client = self._get_obj_client(
1467
+ group_version=resource.group_version, kind=resource.kind
1468
+ )
1369
1469
  try:
1370
1470
  obj = obj_client.get(
1371
1471
  name=name,
@@ -1378,9 +1478,11 @@ class OCNative(OCCli):
1378
1478
  return {}
1379
1479
  raise StatusCodeError(f"[{self.server}]: {e}") from None
1380
1480
 
1381
- def get_all(self, kind, all_namespaces=False):
1382
- k, group_version = self._parse_kind(kind)
1383
- obj_client = self._get_obj_client(group_version=group_version, kind=k)
1481
+ def get_all(self, kind: str, all_namespaces: bool = False) -> dict[str, Any]:
1482
+ resource = self.get_api_resource(kind)
1483
+ obj_client = self._get_obj_client(
1484
+ group_version=resource.group_version, kind=resource.kind
1485
+ )
1384
1486
  try:
1385
1487
  return obj_client.get(_request_timeout=REQUEST_TIMEOUT).to_dict()
1386
1488
  except NotFoundError as e:
@@ -1393,11 +1495,11 @@ OCClient = OCNative | OCCli
1393
1495
  class OCLocal(OCCli):
1394
1496
  def __init__(
1395
1497
  self,
1396
- cluster_name,
1397
- server,
1398
- token,
1399
- local=False,
1400
- ):
1498
+ cluster_name: str,
1499
+ server: str | None,
1500
+ token: str | None,
1501
+ local: bool = False,
1502
+ ) -> None:
1401
1503
  super().__init__(
1402
1504
  cluster_name=cluster_name,
1403
1505
  server=server,
@@ -1406,6 +1508,8 @@ class OCLocal(OCCli):
1406
1508
  )
1407
1509
 
1408
1510
 
1511
+ # we should replace this class with a proper get_oc_client wrapper!
1512
+ # Or getting rid of one or the other OC implementations
1409
1513
  class OC:
1410
1514
  client_status = Counter(
1411
1515
  name="qontract_reconcile_native_client",
@@ -1413,7 +1517,7 @@ class OC:
1413
1517
  labelnames=["cluster_name", "native_client"],
1414
1518
  )
1415
1519
 
1416
- def __new__(
1520
+ def __new__( # type: ignore
1417
1521
  cls,
1418
1522
  cluster_name: str | None = None,
1419
1523
  server: str | None = None,
@@ -1425,7 +1529,7 @@ class OC:
1425
1529
  local: bool = False,
1426
1530
  insecure_skip_tls_verify: bool = False,
1427
1531
  connection_parameters: OCConnectionParameters | None = None,
1428
- ):
1532
+ ) -> OCClient:
1429
1533
  use_native_env = os.environ.get("USE_NATIVE_CLIENT", "")
1430
1534
  use_native = True
1431
1535
  if len(use_native_env) > 0:
@@ -1483,19 +1587,19 @@ class OC_Map: # noqa: N801
1483
1587
 
1484
1588
  def __init__(
1485
1589
  self,
1486
- clusters=None,
1487
- namespaces=None,
1488
- integration="",
1489
- settings=None,
1490
- internal=None,
1491
- use_jump_host=True,
1492
- thread_pool_size=1,
1493
- init_projects=False,
1494
- init_api_resources=False,
1495
- cluster_admin=False,
1496
- ):
1497
- self.oc_map = {}
1498
- self.privileged_oc_map = {}
1590
+ clusters: Iterable[Mapping[str, Any]] | None = None,
1591
+ namespaces: Iterable[Mapping[str, Any]] | None = None,
1592
+ integration: str = "",
1593
+ settings: Mapping[str, Any] | None = None,
1594
+ internal: bool | None = None,
1595
+ use_jump_host: bool = True,
1596
+ thread_pool_size: int = 1,
1597
+ init_projects: bool = False,
1598
+ init_api_resources: bool = False,
1599
+ cluster_admin: bool = False,
1600
+ ) -> None:
1601
+ self.oc_map: dict[str, OCClient | OCLogMsg] = {}
1602
+ self.privileged_oc_map: dict[str, OCClient | OCLogMsg] = {}
1499
1603
  self.calling_integration = integration
1500
1604
  self.settings = settings
1501
1605
  self.internal = internal
@@ -1504,7 +1608,7 @@ class OC_Map: # noqa: N801
1504
1608
  self.init_projects = init_projects
1505
1609
  self.init_api_resources = init_api_resources
1506
1610
  self._lock = Lock()
1507
- self.jh_ports = {}
1611
+ self.jh_ports: dict[str, str | int] = {}
1508
1612
 
1509
1613
  if clusters and namespaces:
1510
1614
  raise KeyError("expected only one of clusters or namespaces.")
@@ -1548,13 +1652,13 @@ class OC_Map: # noqa: N801
1548
1652
  else:
1549
1653
  raise KeyError("expected one of clusters or namespaces.")
1550
1654
 
1551
- def __enter__(self):
1655
+ def __enter__(self) -> OC_Map:
1552
1656
  return self
1553
1657
 
1554
- def __exit__(self, *exc):
1658
+ def __exit__(self, *exc: Any) -> None:
1555
1659
  self.cleanup()
1556
1660
 
1557
- def set_jh_ports(self, jh):
1661
+ def set_jh_ports(self, jh: MutableMapping[str, str | int]) -> None:
1558
1662
  # This will be replaced with getting the data from app-interface in
1559
1663
  # a future PR.
1560
1664
  jh["remotePort"] = 8888
@@ -1565,7 +1669,7 @@ class OC_Map: # noqa: N801
1565
1669
  self.jh_ports[key] = port
1566
1670
  jh["localPort"] = self.jh_ports[key]
1567
1671
 
1568
- def init_oc_client(self, cluster_info, privileged: bool):
1672
+ def init_oc_client(self, cluster_info: Mapping[str, Any], privileged: bool) -> None:
1569
1673
  cluster = cluster_info["name"]
1570
1674
  if not privileged and self.oc_map.get(cluster):
1571
1675
  return None
@@ -1640,15 +1744,18 @@ class OC_Map: # noqa: N801
1640
1744
  if jump_host:
1641
1745
  self.set_jh_ports(jump_host)
1642
1746
  try:
1643
- oc_client = OC(
1644
- cluster,
1645
- server_url,
1646
- token,
1647
- jump_host,
1648
- settings=self.settings,
1649
- init_projects=self.init_projects,
1650
- init_api_resources=self.init_api_resources,
1651
- insecure_skip_tls_verify=insecure_skip_tls_verify,
1747
+ oc_client = cast(
1748
+ "OCClient",
1749
+ OC(
1750
+ cluster,
1751
+ server_url,
1752
+ token,
1753
+ jump_host,
1754
+ settings=self.settings,
1755
+ init_projects=self.init_projects,
1756
+ init_api_resources=self.init_api_resources,
1757
+ insecure_skip_tls_verify=bool(insecure_skip_tls_verify),
1758
+ ),
1652
1759
  )
1653
1760
  self.set_oc(cluster, oc_client, privileged)
1654
1761
  except StatusCodeError as e:
@@ -1661,14 +1768,16 @@ class OC_Map: # noqa: N801
1661
1768
  privileged,
1662
1769
  )
1663
1770
 
1664
- def set_oc(self, cluster: str, value, privileged: bool):
1771
+ def set_oc(
1772
+ self, cluster: str, value: OCClient | OCLogMsg, privileged: bool
1773
+ ) -> None:
1665
1774
  with self._lock:
1666
1775
  if privileged:
1667
1776
  self.privileged_oc_map[cluster] = value
1668
1777
  else:
1669
1778
  self.oc_map[cluster] = value
1670
1779
 
1671
- def cluster_disabled(self, cluster_info):
1780
+ def cluster_disabled(self, cluster_info: Mapping[str, Any]) -> bool:
1672
1781
  try:
1673
1782
  integrations = cluster_info["disable"]["integrations"]
1674
1783
  if self.calling_integration.replace("_", "-") in integrations:
@@ -1678,12 +1787,13 @@ class OC_Map: # noqa: N801
1678
1787
 
1679
1788
  return False
1680
1789
 
1681
- def get(self, cluster: str, privileged: bool = False):
1790
+ def get(self, cluster: str, privileged: bool = False) -> OCClient | OCLogMsg:
1682
1791
  cluster_map = self.privileged_oc_map if privileged else self.oc_map
1683
- return cluster_map.get(
1792
+ c = cluster_map.get(
1684
1793
  cluster,
1685
1794
  OCLogMsg(log_level=logging.DEBUG, message=f"[{cluster}] cluster skipped"),
1686
1795
  )
1796
+ return c
1687
1797
 
1688
1798
  def get_cluster(self, cluster: str, privileged: bool = False) -> OCClient:
1689
1799
  result = self.get(cluster, privileged)
@@ -1705,12 +1815,11 @@ class OC_Map: # noqa: N801
1705
1815
  return list(cluster_map.keys())
1706
1816
  return [k for k, v in cluster_map.items() if v]
1707
1817
 
1708
- def cleanup(self):
1709
- for oc in self.oc_map.values():
1710
- if oc:
1711
- oc.cleanup()
1712
- for oc in self.privileged_oc_map.values():
1713
- if oc:
1818
+ def cleanup(self) -> None:
1819
+ for oc in itertools.chain(
1820
+ self.oc_map.values(), self.privileged_oc_map.values()
1821
+ ):
1822
+ if oc and isinstance(oc, OCClient):
1714
1823
  oc.cleanup()
1715
1824
 
1716
1825
 
@@ -1719,12 +1828,12 @@ class OCLogMsg(Exception): # noqa: N818
1719
1828
  Track log messages associated with initializing OC clients in OC_Map.
1720
1829
  """
1721
1830
 
1722
- def __init__(self, log_level, message):
1831
+ def __init__(self, log_level: int, message: str) -> None:
1723
1832
  super().__init__()
1724
1833
  self.log_level = log_level
1725
1834
  self.message = message
1726
1835
 
1727
- def __bool__(self):
1836
+ def __bool__(self) -> bool:
1728
1837
  """
1729
1838
  Returning False here makes this object falsy, which is used
1730
1839
  elsewhere when differentiating between an OC client or a log
@@ -1741,7 +1850,7 @@ LABEL_MAX_KEY_NAME_LENGTH = 63
1741
1850
  LABEL_MAX_KEY_PREFIX_LENGTH = 253
1742
1851
 
1743
1852
 
1744
- def validate_labels(labels: dict[str, str]) -> Iterable[str]:
1853
+ def validate_labels(labels: Mapping[str, str]) -> list[str]:
1745
1854
  """
1746
1855
  Validate a label key/value against some rules from
1747
1856
  https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
@@ -1803,7 +1912,7 @@ class OpenshiftLazyDiscoverer(LazyDiscoverer):
1803
1912
  https://github.com/openshift/openshift-restclient-python/blob/master/openshift/dynamic/discovery.py
1804
1913
  """
1805
1914
 
1806
- def default_groups(self, request_resources=False):
1915
+ def default_groups(self, request_resources: bool = False) -> dict[str, Any]:
1807
1916
  groups = super().default_groups(request_resources)
1808
1917
  if self.version.get("openshift"):
1809
1918
  groups["oapi"] = {
@@ -1822,7 +1931,7 @@ class OpenshiftLazyDiscoverer(LazyDiscoverer):
1822
1931
  }
1823
1932
  return groups
1824
1933
 
1825
- def get(self, **kwargs):
1934
+ def get(self, **kwargs: Any) -> Any:
1826
1935
  """Same as search, but will throw an error if there are multiple or no
1827
1936
  results. If there are multiple results and only one is an exact match
1828
1937
  on api_version, that resource will be returned.