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
@@ -16,7 +16,7 @@ from collections.abc import (
16
16
  Sequence,
17
17
  )
18
18
  from contextlib import suppress
19
- from datetime import UTC, datetime, timedelta
19
+ from datetime import datetime, timedelta
20
20
  from types import TracebackType
21
21
  from typing import Any
22
22
 
@@ -37,10 +37,12 @@ from sretoolbox.utils import (
37
37
  from reconcile.github_org import get_default_config
38
38
  from reconcile.status import RunningState
39
39
  from reconcile.utils import helm
40
+ from reconcile.utils.datetime_util import utc_now
40
41
  from reconcile.utils.github_api import GithubRepositoryApi
41
42
  from reconcile.utils.gitlab_api import GitLabApi
42
43
  from reconcile.utils.jenkins_api import JenkinsApi, JobBuildState
43
44
  from reconcile.utils.jjb_client import JJB
45
+ from reconcile.utils.json import json_dumps
44
46
  from reconcile.utils.oc import (
45
47
  OCLocal,
46
48
  StatusCodeError,
@@ -90,8 +92,7 @@ from reconcile.utils.state import State
90
92
  from reconcile.utils.vcs import VCS
91
93
 
92
94
  TARGET_CONFIG_HASH = "target_config_hash"
93
-
94
-
95
+ TEMPLATE_API_VERSION = "template.openshift.io/v1"
95
96
  UNIQUE_SAAS_FILE_ENV_COMBO_LEN = 56
96
97
  REQUEST_TIMEOUT = 60
97
98
 
@@ -763,7 +764,8 @@ class SaasHerder:
763
764
  case "gitlab":
764
765
  if not self.gitlab:
765
766
  raise Exception("gitlab is not initialized")
766
- project = self.gitlab.get_project(url)
767
+ if not (project := self.gitlab.get_project(url)):
768
+ raise Exception(f"Could not find gitlab project for {url}")
767
769
  content = self.gitlab.get_raw_file(
768
770
  project=project,
769
771
  path=path,
@@ -799,7 +801,8 @@ class SaasHerder:
799
801
  case "gitlab":
800
802
  if not self.gitlab:
801
803
  raise Exception("gitlab is not initialized")
802
- project = self.gitlab.get_project(url)
804
+ if not (project := self.gitlab.get_project(url)):
805
+ raise Exception(f"Could not find gitlab project for {url}")
803
806
  dir_contents = self.gitlab.get_directory_contents(
804
807
  project,
805
808
  ref=commit_sha,
@@ -824,7 +827,8 @@ class SaasHerder:
824
827
  case "gitlab":
825
828
  if not self.gitlab:
826
829
  raise Exception("gitlab is not initialized")
827
- project = self.gitlab.get_project(url)
830
+ if not (project := self.gitlab.get_project(url)):
831
+ raise Exception(f"Could not find gitlab project for {url}")
828
832
  commits = project.commits.list(ref_name=ref, per_page=1, page=1)
829
833
  return commits[0].id
830
834
  case _:
@@ -869,12 +873,27 @@ class SaasHerder:
869
873
  """
870
874
  if parameter_name in consolidated_parameters:
871
875
  return False
872
- for template_parameter in template.get("parameters", {}):
873
- if template_parameter["name"] == parameter_name:
874
- return True
875
- return False
876
+ return any(
877
+ template_parameter["name"] == parameter_name
878
+ for template_parameter in template.get("parameters") or []
879
+ )
880
+
881
+ @staticmethod
882
+ def _pre_process_template(template: dict[str, Any]) -> dict[str, Any]:
883
+ """
884
+ The only supported apiVersion for OpenShift Template is "template.openshift.io/v1".
885
+ There are examples of templates using "v1", it can't pass validation on 4.19+ oc versions.
876
886
 
877
- def _process_template(self, spec: TargetSpec) -> tuple[list[Any], Promotion | None]:
887
+ Args:
888
+ template (dict): The OpenShift template dictionary.
889
+ Returns:
890
+ dict: The OpenShift template dictionary with the correct apiVersion.
891
+ """
892
+ return template | {"apiVersion": TEMPLATE_API_VERSION}
893
+
894
+ def _process_template(
895
+ self, spec: TargetSpec
896
+ ) -> tuple[Iterable[Any], Promotion | None]:
878
897
  saas_file_name = spec.saas_file_name
879
898
  resource_template_name = spec.resource_template_name
880
899
  url = spec.url
@@ -959,7 +978,10 @@ class SaasHerder:
959
978
 
960
979
  oc = OCLocal("cluster", None, None, local=True)
961
980
  try:
962
- resources = oc.process(template, consolidated_parameters)
981
+ resources: Iterable[Mapping[str, Any]] = oc.process(
982
+ template=self._pre_process_template(template),
983
+ parameters=consolidated_parameters,
984
+ )
963
985
  except StatusCodeError as e:
964
986
  logging.error(f"{error_prefix} error processing template: {e!s}")
965
987
 
@@ -1173,13 +1195,13 @@ class SaasHerder:
1173
1195
  images_list = threaded.run(
1174
1196
  self._collect_images, resources, self.available_thread_pool_size
1175
1197
  )
1176
- images = set(itertools.chain.from_iterable(images_list))
1177
- self.images.update(images)
1178
- if not images:
1198
+ images_set = set(itertools.chain.from_iterable(images_list))
1199
+ self.images.update(images_set)
1200
+ if not images_set:
1179
1201
  return False # no errors
1180
1202
  images = threaded.run(
1181
1203
  self._get_image,
1182
- images,
1204
+ images_set,
1183
1205
  self.available_thread_pool_size,
1184
1206
  image_patterns=spec.image_patterns,
1185
1207
  image_auth=spec.image_auth,
@@ -1244,7 +1266,9 @@ class SaasHerder:
1244
1266
  self.saas_files,
1245
1267
  self.thread_pool_size,
1246
1268
  )
1247
- desired_state_specs = list(itertools.chain.from_iterable(results))
1269
+ desired_state_specs: list[TargetSpec] = list(
1270
+ itertools.chain.from_iterable(results)
1271
+ )
1248
1272
  promotions = threaded.run(
1249
1273
  self.populate_desired_state_saas_file,
1250
1274
  desired_state_specs,
@@ -1861,7 +1885,7 @@ class SaasHerder:
1861
1885
  @staticmethod
1862
1886
  def get_target_config_hash(target_config: Any) -> str:
1863
1887
  m = hashlib.sha256()
1864
- m.update(json.dumps(target_config, sort_keys=True).encode("utf-8"))
1888
+ m.update(json_dumps(target_config).encode("utf-8"))
1865
1889
  digest = m.hexdigest()[:16]
1866
1890
  return digest
1867
1891
 
@@ -1892,21 +1916,23 @@ class SaasHerder:
1892
1916
  name=target.name,
1893
1917
  ref=target.ref,
1894
1918
  promotion=(
1895
- target.promotion.dict(by_alias=True) if target.promotion else None
1919
+ target.promotion.model_dump(by_alias=True) if target.promotion else None
1896
1920
  ),
1897
1921
  secretParameters=(
1898
- [p.dict(by_alias=True) for p in target.secret_parameters]
1922
+ [p.model_dump(by_alias=True) for p in target.secret_parameters]
1899
1923
  if target.secret_parameters
1900
1924
  else None
1901
1925
  ),
1902
1926
  slos=(
1903
- [slo.dict(by_alias=True) for slo in target.slos]
1927
+ [slo.model_dump(by_alias=True) for slo in target.slos]
1904
1928
  if target.slos
1905
1929
  else None
1906
1930
  ),
1907
- upstream=(target.upstream.dict(by_alias=True) if target.upstream else None),
1931
+ upstream=(
1932
+ target.upstream.model_dump(by_alias=True) if target.upstream else None
1933
+ ),
1908
1934
  images=(
1909
- [i.dict(by_alias=True) for i in target.images]
1935
+ [i.model_dump(by_alias=True) for i in target.images]
1910
1936
  if target.images
1911
1937
  else None
1912
1938
  ),
@@ -1917,14 +1943,14 @@ class SaasHerder:
1917
1943
  # before the GQL classes are introduced, the parameters attribute
1918
1944
  # was a json string. Keep it that way to be backwards compatible.
1919
1945
  saas_file_parameters=(
1920
- json.dumps(saas_file.parameters, separators=(",", ":"))
1946
+ json_dumps(saas_file.parameters, compact=True)
1921
1947
  if saas_file.parameters is not None
1922
1948
  else None
1923
1949
  ),
1924
1950
  # before the GQL classes are introduced, the parameters attribute
1925
1951
  # was a json string. Keep it that way to be backwards compatible.
1926
1952
  parameters=(
1927
- json.dumps(target.parameters, separators=(",", ":"))
1953
+ json_dumps(target.parameters, compact=True)
1928
1954
  if target.parameters is not None
1929
1955
  else None
1930
1956
  ),
@@ -1935,23 +1961,23 @@ class SaasHerder:
1935
1961
  # before the GQL classes are introduced, the parameters attribute
1936
1962
  # was a json string. Keep it that way to be backwards compatible.
1937
1963
  rt_parameters=(
1938
- json.dumps(resource_template.parameters, separators=(",", ":"))
1964
+ json_dumps(resource_template.parameters, compact=True)
1939
1965
  if resource_template.parameters is not None
1940
1966
  else None
1941
1967
  ),
1942
1968
  )
1943
1969
  if saas_file.managed_resource_names:
1944
1970
  state_content["saas_file_managed_resource_names"] = [
1945
- m.dict() for m in saas_file.managed_resource_names
1971
+ m.model_dump() for m in saas_file.managed_resource_names
1946
1972
  ]
1947
1973
  # include secret parameters from resource template and saas file
1948
1974
  if resource_template.secret_parameters:
1949
1975
  state_content["rt_secretparameters"] = [
1950
- p.dict() for p in resource_template.secret_parameters
1976
+ p.model_dump() for p in resource_template.secret_parameters
1951
1977
  ]
1952
1978
  if saas_file.secret_parameters:
1953
1979
  state_content["saas_file_secretparameters"] = [
1954
- p.dict() for p in saas_file.secret_parameters
1980
+ p.model_dump() for p in saas_file.secret_parameters
1955
1981
  ]
1956
1982
  return state_content
1957
1983
 
@@ -2020,7 +2046,7 @@ class SaasHerder:
2020
2046
  if promotion.commit_sha in self.hotfix_versions.get(promotion.url, set()):
2021
2047
  return True
2022
2048
 
2023
- now = datetime.now(UTC)
2049
+ now = utc_now()
2024
2050
  passed_soak_days = timedelta(days=0)
2025
2051
 
2026
2052
  for channel in promotion.subscribe:
@@ -2122,7 +2148,7 @@ class SaasHerder:
2122
2148
  if not (self.state and self._promotion_state):
2123
2149
  raise Exception("state is not initialized")
2124
2150
 
2125
- now = datetime.now(UTC)
2151
+ now = utc_now()
2126
2152
  for promotion in self.promotions:
2127
2153
  if promotion is None:
2128
2154
  continue
@@ -2234,7 +2260,9 @@ class SaasHerder:
2234
2260
  for rt in saas_file.resource_templates:
2235
2261
  for target in rt.targets:
2236
2262
  template_vars = {
2237
- "resource": {"namespace": target.namespace.dict(by_alias=True)}
2263
+ "resource": {
2264
+ "namespace": target.namespace.model_dump(by_alias=True)
2265
+ }
2238
2266
  }
2239
2267
  if target.parameters:
2240
2268
  for param in target.parameters:
@@ -146,14 +146,14 @@ class VaultSecretReader(SecretReaderBase):
146
146
  @property
147
147
  def vault_client(self) -> VaultClient:
148
148
  if self._vault_client is None:
149
- self._vault_client = VaultClient()
149
+ self._vault_client = VaultClient.get_instance()
150
150
  return self._vault_client
151
151
 
152
152
  def _read_all(
153
153
  self, path: str, field: str, format: str | None, version: int | None
154
154
  ) -> dict[str, str]:
155
155
  try:
156
- data = self.vault_client.read_all( # type: ignore[attr-defined] # mypy doesn't recognize the VaultClient.__new__ method
156
+ data = self.vault_client.read_all(
157
157
  self._parameters_to_dict(
158
158
  path=path,
159
159
  field=field,
@@ -173,7 +173,7 @@ class VaultSecretReader(SecretReaderBase):
173
173
  self, path: str, field: str, format: str | None, version: int | None
174
174
  ) -> str:
175
175
  try:
176
- data = self.vault_client.read( # type: ignore[attr-defined] # mypy doesn't recognize the VaultClient.__new__ method
176
+ data = self.vault_client.read(
177
177
  self._parameters_to_dict(
178
178
  path=path,
179
179
  field=field,
@@ -251,7 +251,7 @@ class SecretReader(SecretReaderBase):
251
251
  @property
252
252
  def vault_client(self) -> VaultClient:
253
253
  if self._vault_client is None:
254
- self._vault_client = VaultClient()
254
+ self._vault_client = VaultClient.get_instance()
255
255
  return self._vault_client
256
256
 
257
257
  def _read(
@@ -278,7 +278,7 @@ class SecretReader(SecretReaderBase):
278
278
 
279
279
  if self.settings and self.settings.get("vault"):
280
280
  try:
281
- data = self.vault_client.read(params) # type: ignore[attr-defined] # mypy doesn't recognize the VaultClient.__new__ method
281
+ data = self.vault_client.read(params)
282
282
  except vault.SecretNotFoundError as e:
283
283
  raise SecretNotFoundError(*e.args) from e
284
284
  else:
@@ -312,7 +312,7 @@ class SecretReader(SecretReaderBase):
312
312
 
313
313
  if self.settings and self.settings.get("vault"):
314
314
  try:
315
- data = self.vault_client.read_all(params) # type: ignore[attr-defined] # mypy doesn't recognize the VaultClient.__new__ method
315
+ data = self.vault_client.read_all(params)
316
316
  except Forbidden:
317
317
  raise VaultForbiddenError(
318
318
  f"permission denied reading vault secret at {path}"
@@ -8,7 +8,7 @@ SHARDS = int(os.environ.get("SHARDS", "1"))
8
8
  SHARD_ID = int(os.environ.get("SHARD_ID", "0"))
9
9
 
10
10
 
11
- def is_in_shard(value):
11
+ def is_in_shard(value: str) -> bool:
12
12
  if SHARDS == 1:
13
13
  return True
14
14
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import logging
5
+ import os
5
6
  from typing import (
6
7
  TYPE_CHECKING,
7
8
  Any,
@@ -26,6 +27,23 @@ if TYPE_CHECKING:
26
27
  MAX_RETRIES = 5
27
28
  TIMEOUT = 30
28
29
 
30
+ # Slack API base URLs for different workspace types
31
+ SLACK_API_BASE_URL = "https://slack.com/api/"
32
+ SLACK_GOV_API_BASE_URL = "https://slack-gov.com/api/"
33
+
34
+
35
+ def is_gov_slack_workspace() -> bool:
36
+ """
37
+ Determine if a workspace is a government Slack workspace.
38
+
39
+ :return: True if it's a gov-slack workspace, False otherwise
40
+ """
41
+ # Check GOV_SLACK environment variable from OpenShift YAML configuration
42
+ # If not set, defaults to False (regular Slack)
43
+ gov_slack_env = os.getenv("GOV_SLACK", "false")
44
+
45
+ return gov_slack_env.lower() == "true"
46
+
29
47
 
30
48
  class UserNotFoundError(Exception):
31
49
  pass
@@ -53,14 +71,14 @@ class HasClientGlobalConfig(Protocol):
53
71
  max_retries: int | None
54
72
  timeout: int | None
55
73
 
56
- def dict(self) -> dict[str, int | None]: ...
74
+ def model_dump(self) -> dict[str, int | None]: ...
57
75
 
58
76
 
59
77
  class HasClientMethodConfig(Protocol):
60
78
  name: str
61
79
  args: Any
62
80
 
63
- def dict(self) -> dict[str, str]: ...
81
+ def model_dump(self) -> dict[str, str]: ...
64
82
 
65
83
 
66
84
  class HasClientConfig(Protocol):
@@ -165,7 +183,6 @@ class SlackApi:
165
183
  api_config: SlackApiConfig | None = None,
166
184
  init_usergroups: bool = True,
167
185
  channel: str | None = None,
168
- slack_url: str | None = None,
169
186
  **chat_kwargs: Any,
170
187
  ) -> None:
171
188
  """
@@ -187,10 +204,15 @@ class SlackApi:
187
204
  else:
188
205
  self.config = SlackApiConfig()
189
206
 
207
+ # Determine the appropriate Slack API base URL based on GOV_SLACK environment variable
208
+ base_url = (
209
+ SLACK_GOV_API_BASE_URL if is_gov_slack_workspace() else SLACK_API_BASE_URL
210
+ )
211
+
190
212
  self._sc = WebClient(
191
213
  token=token,
192
214
  timeout=self.config.timeout,
193
- base_url=slack_url or WebClient.BASE_URL,
215
+ base_url=base_url,
194
216
  )
195
217
  self._configure_client_retry()
196
218
 
@@ -0,0 +1,224 @@
1
+ import subprocess
2
+ import tempfile
3
+ from io import StringIO
4
+ from typing import Any, NotRequired, TypedDict
5
+
6
+ import yaml
7
+
8
+ from reconcile.utils.ruamel import create_ruamel_instance
9
+
10
+
11
+ class PrometheusRule(TypedDict):
12
+ record: NotRequired[str]
13
+ alert: NotRequired[str]
14
+ expr: str
15
+ labels: NotRequired[dict[str, str]]
16
+ annotations: NotRequired[dict[str, str]]
17
+
18
+
19
+ class PrometheusRuleGroup(TypedDict):
20
+ name: str
21
+ rules: list[PrometheusRule]
22
+
23
+
24
+ class PrometheusRuleSpec(TypedDict):
25
+ groups: list[PrometheusRuleGroup]
26
+
27
+
28
+ class SLOParametersDict(TypedDict):
29
+ window: str
30
+
31
+
32
+ class SLO(TypedDict):
33
+ name: str
34
+ SLIType: str
35
+ SLISpecification: str
36
+ SLOTarget: float
37
+ SLOTargetUnit: str
38
+ SLOParameters: SLOParametersDict
39
+ SLODetails: str
40
+ dashboard: str
41
+ expr: str
42
+ SLIErrorQuery: NotRequired[str]
43
+ SLITotalQuery: NotRequired[str]
44
+
45
+
46
+ class App(TypedDict):
47
+ name: str
48
+
49
+
50
+ class SLODocument(TypedDict):
51
+ name: str
52
+ app: App
53
+ slos: NotRequired[list[SLO]]
54
+
55
+
56
+ class SlothGenerateError(Exception):
57
+ def __init__(self, msg: Any):
58
+ super().__init__("sloth generate failed: " + str(msg))
59
+
60
+
61
+ class SlothInputError(Exception):
62
+ def __init__(self, msg: Any):
63
+ super().__init__("sloth input validation failed: " + str(msg))
64
+
65
+
66
+ def process_sloth_output(output_file_path: str) -> str:
67
+ ruamel_instance = create_ruamel_instance()
68
+ with open(output_file_path, encoding="utf-8") as f:
69
+ data: PrometheusRuleSpec = ruamel_instance.load(f)
70
+ for group in data.get("groups", []):
71
+ for rule in group.get("rules", []):
72
+ labels = (
73
+ # sloth adds several sloth_* labels to alerting rules that are not compliant with prometheus-rule-1 schema
74
+ # see https://sloth.dev/examples/default/getting-started/#__tabbed_1_2
75
+ {k: v for k, v in rule["labels"].items() if not k.startswith("sloth")}
76
+ if rule.get("alert")
77
+ else rule["labels"] # retain all labels on record rules
78
+ )
79
+ annotations = (
80
+ # sloth adds a `title` key within annotations for alert rules: https://sloth.dev/examples/default/getting-started/#__tabbed_1_2
81
+ # this is not compliant with schema and is discarded
82
+ {k: v for k, v in rule["annotations"].items() if k != "title"}
83
+ if rule.get("alert")
84
+ else {} # record rules do not support annotations
85
+ )
86
+ if labels:
87
+ rule["labels"] = labels
88
+ else:
89
+ rule.pop("labels", None)
90
+ if annotations:
91
+ rule["annotations"] = annotations
92
+ else:
93
+ rule.pop("annotations", None)
94
+ with StringIO() as s:
95
+ ruamel_instance.dump(data, s)
96
+ return s.getvalue()
97
+
98
+
99
+ def run_sloth(spec: dict[str, Any]) -> str:
100
+ with (
101
+ tempfile.NamedTemporaryFile(
102
+ encoding="utf-8", mode="w", suffix=".yml"
103
+ ) as input_file,
104
+ tempfile.NamedTemporaryFile(
105
+ encoding="utf-8", mode="w", suffix=".yml"
106
+ ) as output_file,
107
+ ):
108
+ yaml.dump(spec, input_file, allow_unicode=True)
109
+ cmd = ["sloth", "generate", "-i", input_file.name, "-o", output_file.name]
110
+ try:
111
+ subprocess.run(cmd, capture_output=True, check=True, text=True)
112
+ except subprocess.CalledProcessError as e:
113
+ error_msg = f"{e}"
114
+ if e.stdout:
115
+ error_msg += f"\nstdout: {e.stdout}"
116
+ if e.stderr:
117
+ error_msg += f"\nstderr: {e.stderr}"
118
+ raise SlothGenerateError(error_msg) from e
119
+ return process_sloth_output(output_file.name)
120
+
121
+
122
+ def get_slo_target(slo: SLO) -> float:
123
+ """
124
+ Ensure SLO target unit aligns with format expected by sloth for 'Objective' attribute
125
+ https://pkg.go.dev/github.com/slok/sloth/pkg/prometheus/api/v1#section-readme
126
+ """
127
+ val = float(slo["SLOTarget"])
128
+ return val * (100.0 if slo.get("SLOTargetUnit") == "percent_0_1" else 1.0)
129
+
130
+
131
+ def generate_sloth_rules(
132
+ slo_document: SLODocument,
133
+ version: str = "prometheus/v1",
134
+ ) -> str:
135
+ """Generate Prometheus rules for an slo_document_v1 using sloth
136
+
137
+ Args:
138
+ slo_document query:
139
+ {
140
+ slo_docs: slo_document_v1(filter: {name: "foo"}) {
141
+ name
142
+ app {
143
+ name
144
+ }
145
+ slos {
146
+ name
147
+ SLIType
148
+ SLOTargetUnit
149
+ SLOParameters {
150
+ window
151
+ }
152
+ expr
153
+ SLOTarget
154
+ SLIErrorQuery
155
+ SLITotalQuery
156
+ SLODetails
157
+ dashboard
158
+ }
159
+ }
160
+ }
161
+ version: Spec version (default: "prometheus/v1")
162
+
163
+ Returns:
164
+ Generated Prometheus rules as YAML string
165
+ """
166
+ if not slo_document.get("slos"):
167
+ raise SlothInputError("SLO document has no SLOs defined")
168
+
169
+ service = slo_document["app"]["name"]
170
+ # only process SLOs that have both error and total queries defined
171
+ slo_input = [
172
+ {
173
+ "name": slo["name"],
174
+ "objective": get_slo_target(slo),
175
+ "description": f"{slo['name']} SLO for {service}",
176
+ "sli": {
177
+ "events": {
178
+ "error_query": slo["SLIErrorQuery"].replace(
179
+ "{{window}}", "{{.window}}"
180
+ ),
181
+ "total_query": slo["SLITotalQuery"].replace(
182
+ "{{window}}", "{{.window}}"
183
+ ),
184
+ }
185
+ },
186
+ "alerting": {
187
+ "name": f"{service.title()}{slo['name'].title()}",
188
+ "annotations": {
189
+ "summary": f"High error rate on {service} {slo['name']}",
190
+ "message": f"High error rate on {service} {slo['name']}",
191
+ "runbook": slo["SLODetails"],
192
+ "dashboard": slo["dashboard"],
193
+ },
194
+ "page_alert": {
195
+ "labels": {
196
+ "severity": "critical",
197
+ "service": service,
198
+ "slo": slo["name"],
199
+ }
200
+ },
201
+ "ticket_alert": {
202
+ "labels": {
203
+ "severity": "high",
204
+ "service": service,
205
+ "slo": slo["name"],
206
+ }
207
+ },
208
+ },
209
+ }
210
+ for slo in slo_document["slos"]
211
+ if slo.get("SLIErrorQuery") and slo.get("SLITotalQuery")
212
+ ]
213
+
214
+ if not slo_input:
215
+ raise SlothInputError(
216
+ "No SLOs found with both SLIErrorQuery and SLITotalQuery defined"
217
+ )
218
+
219
+ spec = {
220
+ "version": version,
221
+ "service": service,
222
+ "slos": slo_input,
223
+ }
224
+ return run_sloth(spec)
@@ -1,7 +1,10 @@
1
1
  import json
2
2
  import os
3
+ from collections.abc import Iterable, Mapping
4
+ from typing import Any, Self
3
5
 
4
6
  from reconcile.utils.aws_api import AWSApi
7
+ from reconcile.utils.json import json_dumps
5
8
  from reconcile.utils.secret_reader import SecretReader
6
9
 
7
10
 
@@ -12,7 +15,9 @@ class SQSGatewayInitError(Exception):
12
15
  class SQSGateway:
13
16
  """Wrapper around SQS AWS SDK"""
14
17
 
15
- def __init__(self, accounts, secret_reader: SecretReader):
18
+ def __init__(
19
+ self, accounts: Iterable[Mapping[str, Any]], secret_reader: SecretReader
20
+ ) -> None:
16
21
  queue_url = os.environ.get("gitlab_pr_submitter_queue_url") # noqa: SIM112
17
22
  if not queue_url:
18
23
  raise SQSGatewayInitError(
@@ -30,17 +35,17 @@ class SQSGateway:
30
35
  self.sqs = self._aws_api.get_session_client(session, "sqs")
31
36
  self.queue_url = queue_url
32
37
 
33
- def __enter__(self):
38
+ def __enter__(self) -> Self:
34
39
  return self
35
40
 
36
- def __exit__(self, *ext):
41
+ def __exit__(self, *ext: Any) -> None:
37
42
  self.cleanup()
38
43
 
39
- def cleanup(self):
44
+ def cleanup(self) -> None:
40
45
  self._aws_api.cleanup()
41
46
 
42
47
  @staticmethod
43
- def get_queue_account(accounts, queue_url):
48
+ def get_queue_account(accounts: Iterable[Mapping[str, Any]], queue_url: str) -> str:
44
49
  queue_account_uid = queue_url.split("/")[3]
45
50
  queue_account_name = [
46
51
  a["name"] for a in accounts if a["uid"] == queue_account_uid
@@ -49,14 +54,14 @@ class SQSGateway:
49
54
  raise SQSGatewayInitError(f"account uid not found: {queue_account_uid}")
50
55
  return queue_account_name[0]
51
56
 
52
- def send_message(self, body):
53
- self.sqs.send_message(QueueUrl=self.queue_url, MessageBody=json.dumps(body))
57
+ def send_message(self, body: Mapping[str, Any]) -> None:
58
+ self.sqs.send_message(QueueUrl=self.queue_url, MessageBody=json_dumps(body))
54
59
 
55
60
  def receive_messages(
56
61
  self,
57
- visibility_timeout=30,
58
- wait_time_seconds=20,
59
- ):
62
+ visibility_timeout: int = 30,
63
+ wait_time_seconds: int = 20,
64
+ ) -> list[tuple[str, dict[str, Any]]]:
60
65
  messages = self.sqs.receive_message(
61
66
  QueueUrl=self.queue_url,
62
67
  VisibilityTimeout=visibility_timeout,
@@ -64,5 +69,5 @@ class SQSGateway:
64
69
  ).get("Messages", [])
65
70
  return [(m["ReceiptHandle"], json.loads(m["Body"])) for m in messages]
66
71
 
67
- def delete_message(self, receipt_handle):
72
+ def delete_message(self, receipt_handle: str) -> None:
68
73
  self.sqs.delete_message(QueueUrl=self.queue_url, ReceiptHandle=receipt_handle)
reconcile/utils/state.py CHANGED
@@ -28,6 +28,7 @@ from reconcile.typed_queries.app_interface_vault_settings import (
28
28
  )
29
29
  from reconcile.typed_queries.get_state_aws_account import get_state_aws_account
30
30
  from reconcile.utils.aws_api import aws_config_file_path
31
+ from reconcile.utils.json import json_dumps
31
32
  from reconcile.utils.secret_reader import (
32
33
  SecretReaderBase,
33
34
  create_secret_reader,
@@ -355,7 +356,7 @@ class State:
355
356
  self.client.put_object(
356
357
  Bucket=self.bucket,
357
358
  Key=f"{self.state_path}/{key}",
358
- Body=json.dumps(value),
359
+ Body=json_dumps(value),
359
360
  Metadata=metadata or {},
360
361
  )
361
362