qontract-reconcile 0.9.1rc298__py3-none-any.whl → 0.10.1.dev1203__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (843) hide show
  1. qontract_reconcile-0.10.1.dev1203.dist-info/METADATA +500 -0
  2. qontract_reconcile-0.10.1.dev1203.dist-info/RECORD +771 -0
  3. {qontract_reconcile-0.9.1rc298.dist-info → qontract_reconcile-0.10.1.dev1203.dist-info}/WHEEL +1 -2
  4. {qontract_reconcile-0.9.1rc298.dist-info → qontract_reconcile-0.10.1.dev1203.dist-info}/entry_points.txt +4 -2
  5. reconcile/acs_notifiers.py +126 -0
  6. reconcile/acs_policies.py +243 -0
  7. reconcile/acs_rbac.py +596 -0
  8. reconcile/aus/advanced_upgrade_service.py +621 -8
  9. reconcile/aus/aus_label_source.py +115 -0
  10. reconcile/aus/base.py +1053 -353
  11. reconcile/{utils → aus}/cluster_version_data.py +27 -12
  12. reconcile/aus/healthchecks.py +77 -0
  13. reconcile/aus/metrics.py +158 -0
  14. reconcile/aus/models.py +245 -5
  15. reconcile/aus/node_pool_spec.py +35 -0
  16. reconcile/aus/ocm_addons_upgrade_scheduler_org.py +225 -110
  17. reconcile/aus/ocm_upgrade_scheduler.py +76 -71
  18. reconcile/aus/ocm_upgrade_scheduler_org.py +81 -23
  19. reconcile/aus/version_gate_approver.py +204 -0
  20. reconcile/aus/version_gates/__init__.py +12 -0
  21. reconcile/aus/version_gates/handler.py +33 -0
  22. reconcile/aus/version_gates/ingress_gate_handler.py +32 -0
  23. reconcile/aus/version_gates/ocp_gate_handler.py +26 -0
  24. reconcile/aus/version_gates/sts_version_gate_handler.py +100 -0
  25. reconcile/aws_account_manager/README.md +5 -0
  26. reconcile/aws_account_manager/integration.py +373 -0
  27. reconcile/aws_account_manager/merge_request_manager.py +114 -0
  28. reconcile/aws_account_manager/metrics.py +39 -0
  29. reconcile/aws_account_manager/reconciler.py +403 -0
  30. reconcile/aws_account_manager/utils.py +41 -0
  31. reconcile/aws_ami_cleanup/integration.py +273 -0
  32. reconcile/aws_ami_share.py +18 -14
  33. reconcile/aws_cloudwatch_log_retention/integration.py +253 -0
  34. reconcile/aws_iam_keys.py +1 -1
  35. reconcile/aws_iam_password_reset.py +56 -20
  36. reconcile/aws_saml_idp/integration.py +204 -0
  37. reconcile/aws_saml_roles/integration.py +322 -0
  38. reconcile/aws_support_cases_sos.py +2 -2
  39. reconcile/aws_version_sync/integration.py +430 -0
  40. reconcile/aws_version_sync/merge_request_manager/merge_request.py +156 -0
  41. reconcile/aws_version_sync/merge_request_manager/merge_request_manager.py +160 -0
  42. reconcile/aws_version_sync/utils.py +64 -0
  43. reconcile/blackbox_exporter_endpoint_monitoring.py +2 -5
  44. reconcile/change_owners/README.md +34 -0
  45. reconcile/change_owners/approver.py +7 -9
  46. reconcile/change_owners/bundle.py +134 -9
  47. reconcile/change_owners/change_log_tracking.py +236 -0
  48. reconcile/change_owners/change_owners.py +204 -194
  49. reconcile/change_owners/change_types.py +183 -265
  50. reconcile/change_owners/changes.py +488 -0
  51. reconcile/change_owners/decision.py +120 -41
  52. reconcile/change_owners/diff.py +63 -92
  53. reconcile/change_owners/implicit_ownership.py +19 -16
  54. reconcile/change_owners/self_service_roles.py +158 -35
  55. reconcile/change_owners/tester.py +20 -18
  56. reconcile/checkpoint.py +4 -6
  57. reconcile/cli.py +1523 -242
  58. reconcile/closedbox_endpoint_monitoring_base.py +10 -17
  59. reconcile/cluster_auth_rhidp/integration.py +257 -0
  60. reconcile/cluster_deployment_mapper.py +2 -5
  61. reconcile/cna/assets/asset.py +4 -7
  62. reconcile/cna/assets/null.py +2 -5
  63. reconcile/cna/integration.py +2 -3
  64. reconcile/cna/state.py +6 -9
  65. reconcile/dashdotdb_base.py +31 -10
  66. reconcile/dashdotdb_cso.py +3 -6
  67. reconcile/dashdotdb_dora.py +530 -0
  68. reconcile/dashdotdb_dvo.py +10 -13
  69. reconcile/dashdotdb_slo.py +75 -19
  70. reconcile/database_access_manager.py +753 -0
  71. reconcile/deadmanssnitch.py +207 -0
  72. reconcile/dynatrace_token_provider/dependencies.py +69 -0
  73. reconcile/dynatrace_token_provider/integration.py +656 -0
  74. reconcile/dynatrace_token_provider/metrics.py +62 -0
  75. reconcile/dynatrace_token_provider/model.py +14 -0
  76. reconcile/dynatrace_token_provider/ocm.py +140 -0
  77. reconcile/dynatrace_token_provider/validate.py +48 -0
  78. reconcile/endpoints_discovery/integration.py +348 -0
  79. reconcile/endpoints_discovery/merge_request.py +96 -0
  80. reconcile/endpoints_discovery/merge_request_manager.py +178 -0
  81. reconcile/external_resources/aws.py +204 -0
  82. reconcile/external_resources/factories.py +163 -0
  83. reconcile/external_resources/integration.py +194 -0
  84. reconcile/external_resources/integration_secrets_sync.py +47 -0
  85. reconcile/external_resources/manager.py +405 -0
  86. reconcile/external_resources/meta.py +17 -0
  87. reconcile/external_resources/metrics.py +95 -0
  88. reconcile/external_resources/model.py +350 -0
  89. reconcile/external_resources/reconciler.py +265 -0
  90. reconcile/external_resources/secrets_sync.py +465 -0
  91. reconcile/external_resources/state.py +258 -0
  92. reconcile/gabi_authorized_users.py +19 -11
  93. reconcile/gcr_mirror.py +43 -34
  94. reconcile/github_org.py +4 -6
  95. reconcile/github_owners.py +1 -1
  96. reconcile/github_repo_invites.py +2 -5
  97. reconcile/gitlab_fork_compliance.py +14 -13
  98. reconcile/gitlab_housekeeping.py +185 -91
  99. reconcile/gitlab_labeler.py +15 -14
  100. reconcile/gitlab_members.py +126 -120
  101. reconcile/gitlab_owners.py +53 -66
  102. reconcile/gitlab_permissions.py +167 -6
  103. reconcile/glitchtip/README.md +150 -0
  104. reconcile/glitchtip/integration.py +99 -51
  105. reconcile/glitchtip/reconciler.py +99 -70
  106. reconcile/glitchtip_project_alerts/__init__.py +0 -0
  107. reconcile/glitchtip_project_alerts/integration.py +333 -0
  108. reconcile/glitchtip_project_dsn/integration.py +43 -43
  109. reconcile/gql_definitions/acs/__init__.py +0 -0
  110. reconcile/gql_definitions/acs/acs_instances.py +83 -0
  111. reconcile/gql_definitions/acs/acs_policies.py +239 -0
  112. reconcile/gql_definitions/acs/acs_rbac.py +111 -0
  113. reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py +46 -8
  114. reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py +38 -8
  115. reconcile/gql_definitions/app_interface_metrics_exporter/__init__.py +0 -0
  116. reconcile/gql_definitions/app_interface_metrics_exporter/onboarding_status.py +61 -0
  117. reconcile/gql_definitions/aws_account_manager/__init__.py +0 -0
  118. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +177 -0
  119. reconcile/gql_definitions/aws_ami_cleanup/__init__.py +0 -0
  120. reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +161 -0
  121. reconcile/gql_definitions/aws_saml_idp/__init__.py +0 -0
  122. reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +117 -0
  123. reconcile/gql_definitions/aws_saml_roles/__init__.py +0 -0
  124. reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +117 -0
  125. reconcile/gql_definitions/aws_saml_roles/roles.py +97 -0
  126. reconcile/gql_definitions/aws_version_sync/__init__.py +0 -0
  127. reconcile/gql_definitions/aws_version_sync/clusters.py +83 -0
  128. reconcile/gql_definitions/aws_version_sync/namespaces.py +143 -0
  129. reconcile/gql_definitions/change_owners/queries/change_types.py +16 -29
  130. reconcile/gql_definitions/change_owners/queries/self_service_roles.py +45 -11
  131. reconcile/gql_definitions/cluster_auth_rhidp/__init__.py +0 -0
  132. reconcile/gql_definitions/cluster_auth_rhidp/clusters.py +128 -0
  133. reconcile/gql_definitions/cna/queries/cna_provisioners.py +6 -8
  134. reconcile/gql_definitions/cna/queries/cna_resources.py +3 -5
  135. reconcile/gql_definitions/common/alerting_services_settings.py +2 -2
  136. reconcile/gql_definitions/common/app_code_component_repos.py +9 -5
  137. reconcile/gql_definitions/{glitchtip/glitchtip_settings.py → common/app_interface_custom_messages.py} +14 -16
  138. reconcile/gql_definitions/common/app_interface_dms_settings.py +86 -0
  139. reconcile/gql_definitions/common/app_interface_repo_settings.py +2 -2
  140. reconcile/gql_definitions/common/app_interface_state_settings.py +3 -5
  141. reconcile/gql_definitions/common/app_interface_vault_settings.py +3 -5
  142. reconcile/gql_definitions/common/app_quay_repos_escalation_policies.py +120 -0
  143. reconcile/gql_definitions/common/apps.py +72 -0
  144. reconcile/gql_definitions/common/aws_vpc_requests.py +109 -0
  145. reconcile/gql_definitions/common/aws_vpcs.py +84 -0
  146. reconcile/gql_definitions/common/clusters.py +120 -254
  147. reconcile/gql_definitions/common/clusters_minimal.py +11 -35
  148. reconcile/gql_definitions/common/clusters_with_dms.py +72 -0
  149. reconcile/gql_definitions/common/clusters_with_peering.py +70 -98
  150. reconcile/gql_definitions/common/github_orgs.py +2 -2
  151. reconcile/gql_definitions/common/jira_settings.py +68 -0
  152. reconcile/gql_definitions/common/jiralert_settings.py +68 -0
  153. reconcile/gql_definitions/common/namespaces.py +74 -32
  154. reconcile/gql_definitions/common/namespaces_minimal.py +4 -10
  155. reconcile/gql_definitions/common/ocm_env_telemeter.py +95 -0
  156. reconcile/gql_definitions/common/ocm_environments.py +4 -2
  157. reconcile/gql_definitions/common/pagerduty_instances.py +5 -5
  158. reconcile/gql_definitions/common/pgp_reencryption_settings.py +5 -11
  159. reconcile/gql_definitions/common/pipeline_providers.py +45 -90
  160. reconcile/gql_definitions/common/quay_instances.py +64 -0
  161. reconcile/gql_definitions/common/quay_orgs.py +68 -0
  162. reconcile/gql_definitions/common/reserved_networks.py +94 -0
  163. reconcile/gql_definitions/common/saas_files.py +133 -95
  164. reconcile/gql_definitions/common/saas_target_namespaces.py +41 -26
  165. reconcile/gql_definitions/common/saasherder_settings.py +2 -2
  166. reconcile/gql_definitions/common/slack_workspaces.py +62 -0
  167. reconcile/gql_definitions/common/smtp_client_settings.py +2 -2
  168. reconcile/gql_definitions/common/state_aws_account.py +77 -0
  169. reconcile/gql_definitions/common/users.py +3 -2
  170. reconcile/gql_definitions/cost_report/__init__.py +0 -0
  171. reconcile/gql_definitions/cost_report/app_names.py +68 -0
  172. reconcile/gql_definitions/cost_report/cost_namespaces.py +86 -0
  173. reconcile/gql_definitions/cost_report/settings.py +77 -0
  174. reconcile/gql_definitions/dashdotdb_slo/slo_documents_query.py +42 -12
  175. reconcile/gql_definitions/dynatrace_token_provider/__init__.py +0 -0
  176. reconcile/gql_definitions/dynatrace_token_provider/dynatrace_bootstrap_tokens.py +79 -0
  177. reconcile/gql_definitions/dynatrace_token_provider/token_specs.py +84 -0
  178. reconcile/gql_definitions/endpoints_discovery/__init__.py +0 -0
  179. reconcile/gql_definitions/endpoints_discovery/namespaces.py +127 -0
  180. reconcile/gql_definitions/external_resources/__init__.py +0 -0
  181. reconcile/gql_definitions/external_resources/aws_accounts.py +73 -0
  182. reconcile/gql_definitions/external_resources/external_resources_modules.py +78 -0
  183. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +1111 -0
  184. reconcile/gql_definitions/external_resources/external_resources_settings.py +98 -0
  185. reconcile/gql_definitions/fragments/aus_organization.py +34 -39
  186. reconcile/gql_definitions/fragments/aws_account_common.py +62 -0
  187. reconcile/gql_definitions/fragments/aws_account_managed.py +57 -0
  188. reconcile/gql_definitions/fragments/aws_account_sso.py +35 -0
  189. reconcile/gql_definitions/fragments/aws_infra_management_account.py +2 -2
  190. reconcile/gql_definitions/fragments/aws_vpc.py +47 -0
  191. reconcile/gql_definitions/fragments/aws_vpc_request.py +65 -0
  192. reconcile/gql_definitions/fragments/aws_vpc_request_subnet.py +29 -0
  193. reconcile/gql_definitions/fragments/deplopy_resources.py +7 -7
  194. reconcile/gql_definitions/fragments/disable.py +28 -0
  195. reconcile/gql_definitions/fragments/jumphost_common_fields.py +2 -2
  196. reconcile/gql_definitions/fragments/membership_source.py +47 -0
  197. reconcile/gql_definitions/fragments/minimal_ocm_organization.py +29 -0
  198. reconcile/gql_definitions/fragments/oc_connection_cluster.py +4 -9
  199. reconcile/gql_definitions/fragments/ocm_environment.py +5 -5
  200. reconcile/gql_definitions/fragments/pipeline_provider_retention.py +30 -0
  201. reconcile/gql_definitions/fragments/prometheus_instance.py +48 -0
  202. reconcile/gql_definitions/fragments/resource_limits_requirements.py +29 -0
  203. reconcile/gql_definitions/fragments/{resource_requirements.py → resource_requests_requirements.py} +3 -3
  204. reconcile/gql_definitions/fragments/resource_values.py +2 -2
  205. reconcile/gql_definitions/fragments/saas_target_namespace.py +55 -12
  206. reconcile/gql_definitions/fragments/serviceaccount_token.py +38 -0
  207. reconcile/gql_definitions/fragments/terraform_state.py +36 -0
  208. reconcile/gql_definitions/fragments/upgrade_policy.py +5 -3
  209. reconcile/gql_definitions/fragments/user.py +3 -2
  210. reconcile/gql_definitions/fragments/vault_secret.py +2 -2
  211. reconcile/gql_definitions/gitlab_members/gitlab_instances.py +6 -2
  212. reconcile/gql_definitions/gitlab_members/permissions.py +3 -5
  213. reconcile/gql_definitions/glitchtip/glitchtip_instance.py +16 -2
  214. reconcile/gql_definitions/glitchtip/glitchtip_project.py +22 -23
  215. reconcile/gql_definitions/glitchtip_project_alerts/__init__.py +0 -0
  216. reconcile/gql_definitions/glitchtip_project_alerts/glitchtip_project.py +173 -0
  217. reconcile/gql_definitions/integrations/integrations.py +62 -45
  218. reconcile/gql_definitions/introspection.json +51176 -0
  219. reconcile/gql_definitions/jenkins_configs/jenkins_configs.py +13 -5
  220. reconcile/gql_definitions/jenkins_configs/jenkins_instances.py +79 -0
  221. reconcile/gql_definitions/jira/__init__.py +0 -0
  222. reconcile/gql_definitions/jira/jira_servers.py +80 -0
  223. reconcile/gql_definitions/jira_permissions_validator/__init__.py +0 -0
  224. reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +131 -0
  225. reconcile/gql_definitions/jumphosts/jumphosts.py +3 -5
  226. reconcile/gql_definitions/ldap_groups/__init__.py +0 -0
  227. reconcile/gql_definitions/ldap_groups/roles.py +111 -0
  228. reconcile/gql_definitions/ldap_groups/settings.py +79 -0
  229. reconcile/gql_definitions/maintenance/__init__.py +0 -0
  230. reconcile/gql_definitions/maintenance/maintenances.py +101 -0
  231. reconcile/gql_definitions/membershipsources/__init__.py +0 -0
  232. reconcile/gql_definitions/membershipsources/roles.py +112 -0
  233. reconcile/gql_definitions/ocm_labels/__init__.py +0 -0
  234. reconcile/gql_definitions/ocm_labels/clusters.py +112 -0
  235. reconcile/gql_definitions/ocm_labels/organizations.py +78 -0
  236. reconcile/gql_definitions/ocm_subscription_labels/__init__.py +0 -0
  237. reconcile/gql_definitions/openshift_cluster_bots/__init__.py +0 -0
  238. reconcile/gql_definitions/openshift_cluster_bots/clusters.py +126 -0
  239. reconcile/gql_definitions/openshift_groups/managed_groups.py +2 -2
  240. reconcile/gql_definitions/openshift_groups/managed_roles.py +3 -2
  241. reconcile/gql_definitions/openshift_serviceaccount_tokens/__init__.py +0 -0
  242. reconcile/gql_definitions/openshift_serviceaccount_tokens/tokens.py +132 -0
  243. reconcile/gql_definitions/quay_membership/quay_membership.py +3 -5
  244. reconcile/gql_definitions/rhidp/__init__.py +0 -0
  245. reconcile/gql_definitions/rhidp/organizations.py +96 -0
  246. reconcile/gql_definitions/service_dependencies/jenkins_instance_fragment.py +2 -2
  247. reconcile/gql_definitions/service_dependencies/service_dependencies.py +9 -31
  248. reconcile/gql_definitions/sharding/aws_accounts.py +2 -2
  249. reconcile/gql_definitions/sharding/ocm_organization.py +63 -0
  250. reconcile/gql_definitions/skupper_network/site_controller_template.py +2 -2
  251. reconcile/gql_definitions/skupper_network/skupper_networks.py +12 -38
  252. reconcile/gql_definitions/slack_usergroups/clusters.py +2 -2
  253. reconcile/gql_definitions/slack_usergroups/permissions.py +8 -15
  254. reconcile/gql_definitions/slack_usergroups/users.py +3 -2
  255. reconcile/gql_definitions/slo_documents/__init__.py +0 -0
  256. reconcile/gql_definitions/slo_documents/slo_documents.py +142 -0
  257. reconcile/gql_definitions/status_board/__init__.py +0 -0
  258. reconcile/gql_definitions/status_board/status_board.py +163 -0
  259. reconcile/gql_definitions/statuspage/statuspages.py +56 -7
  260. reconcile/gql_definitions/templating/__init__.py +0 -0
  261. reconcile/gql_definitions/templating/template_collection.py +130 -0
  262. reconcile/gql_definitions/templating/templates.py +108 -0
  263. reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py +4 -8
  264. reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py +8 -8
  265. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_accounts.py +6 -8
  266. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_resources.py +45 -56
  267. reconcile/gql_definitions/terraform_cloudflare_users/app_interface_setting_cloudflare_and_vault.py +4 -8
  268. reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.py +4 -8
  269. reconcile/gql_definitions/terraform_init/__init__.py +0 -0
  270. reconcile/gql_definitions/terraform_init/aws_accounts.py +93 -0
  271. reconcile/gql_definitions/terraform_repo/__init__.py +0 -0
  272. reconcile/gql_definitions/terraform_repo/terraform_repo.py +141 -0
  273. reconcile/gql_definitions/terraform_resources/database_access_manager.py +158 -0
  274. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +153 -162
  275. reconcile/gql_definitions/terraform_tgw_attachments/__init__.py +0 -0
  276. reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +119 -0
  277. reconcile/gql_definitions/unleash_feature_toggles/__init__.py +0 -0
  278. reconcile/gql_definitions/unleash_feature_toggles/feature_toggles.py +113 -0
  279. reconcile/gql_definitions/vault_instances/vault_instances.py +17 -50
  280. reconcile/gql_definitions/vault_policies/vault_policies.py +2 -2
  281. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator.py +49 -12
  282. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator_peered_cluster_fragment.py +7 -2
  283. reconcile/integrations_manager.py +25 -13
  284. reconcile/jenkins/types.py +5 -1
  285. reconcile/jenkins_base.py +36 -0
  286. reconcile/jenkins_job_builder.py +10 -48
  287. reconcile/jenkins_job_builds_cleaner.py +40 -25
  288. reconcile/jenkins_job_cleaner.py +1 -3
  289. reconcile/jenkins_roles.py +22 -26
  290. reconcile/jenkins_webhooks.py +9 -6
  291. reconcile/jenkins_worker_fleets.py +11 -6
  292. reconcile/jira_permissions_validator.py +340 -0
  293. reconcile/jira_watcher.py +3 -5
  294. reconcile/ldap_groups/__init__.py +0 -0
  295. reconcile/ldap_groups/integration.py +279 -0
  296. reconcile/ldap_users.py +3 -0
  297. reconcile/ocm/types.py +39 -59
  298. reconcile/ocm_additional_routers.py +0 -1
  299. reconcile/ocm_addons_upgrade_tests_trigger.py +10 -15
  300. reconcile/ocm_aws_infrastructure_access.py +30 -32
  301. reconcile/ocm_clusters.py +217 -130
  302. reconcile/ocm_external_configuration_labels.py +15 -0
  303. reconcile/ocm_github_idp.py +1 -1
  304. reconcile/ocm_groups.py +25 -5
  305. reconcile/ocm_internal_notifications/__init__.py +0 -0
  306. reconcile/ocm_internal_notifications/integration.py +119 -0
  307. reconcile/ocm_labels/__init__.py +0 -0
  308. reconcile/ocm_labels/integration.py +409 -0
  309. reconcile/ocm_machine_pools.py +517 -108
  310. reconcile/ocm_upgrade_scheduler_org_updater.py +15 -11
  311. reconcile/openshift_base.py +609 -207
  312. reconcile/openshift_cluster_bots.py +344 -0
  313. reconcile/openshift_clusterrolebindings.py +15 -15
  314. reconcile/openshift_groups.py +42 -45
  315. reconcile/openshift_limitranges.py +1 -0
  316. reconcile/openshift_namespace_labels.py +22 -28
  317. reconcile/openshift_namespaces.py +22 -22
  318. reconcile/openshift_network_policies.py +4 -8
  319. reconcile/openshift_prometheus_rules.py +43 -0
  320. reconcile/openshift_resourcequotas.py +2 -16
  321. reconcile/openshift_resources.py +12 -10
  322. reconcile/openshift_resources_base.py +304 -328
  323. reconcile/openshift_rolebindings.py +18 -20
  324. reconcile/openshift_saas_deploy.py +105 -21
  325. reconcile/openshift_saas_deploy_change_tester.py +30 -35
  326. reconcile/openshift_saas_deploy_trigger_base.py +39 -36
  327. reconcile/openshift_saas_deploy_trigger_cleaner.py +41 -27
  328. reconcile/openshift_saas_deploy_trigger_configs.py +1 -2
  329. reconcile/openshift_saas_deploy_trigger_images.py +1 -2
  330. reconcile/openshift_saas_deploy_trigger_moving_commits.py +1 -2
  331. reconcile/openshift_saas_deploy_trigger_upstream_jobs.py +1 -2
  332. reconcile/openshift_serviceaccount_tokens.py +138 -74
  333. reconcile/openshift_tekton_resources.py +89 -24
  334. reconcile/openshift_upgrade_watcher.py +110 -62
  335. reconcile/openshift_users.py +16 -15
  336. reconcile/openshift_vault_secrets.py +11 -6
  337. reconcile/oum/__init__.py +0 -0
  338. reconcile/oum/base.py +387 -0
  339. reconcile/oum/labelset.py +55 -0
  340. reconcile/oum/metrics.py +71 -0
  341. reconcile/oum/models.py +69 -0
  342. reconcile/oum/providers.py +59 -0
  343. reconcile/oum/standalone.py +196 -0
  344. reconcile/prometheus_rules_tester/integration.py +31 -23
  345. reconcile/quay_base.py +4 -1
  346. reconcile/quay_membership.py +1 -2
  347. reconcile/quay_mirror.py +111 -61
  348. reconcile/quay_mirror_org.py +34 -21
  349. reconcile/quay_permissions.py +7 -3
  350. reconcile/quay_repos.py +24 -32
  351. reconcile/queries.py +263 -198
  352. reconcile/query_validator.py +3 -5
  353. reconcile/resource_scraper.py +3 -4
  354. reconcile/{template_tester.py → resource_template_tester.py} +3 -3
  355. reconcile/rhidp/__init__.py +0 -0
  356. reconcile/rhidp/common.py +214 -0
  357. reconcile/rhidp/metrics.py +20 -0
  358. reconcile/rhidp/ocm_oidc_idp/__init__.py +0 -0
  359. reconcile/rhidp/ocm_oidc_idp/base.py +221 -0
  360. reconcile/rhidp/ocm_oidc_idp/integration.py +56 -0
  361. reconcile/rhidp/ocm_oidc_idp/metrics.py +22 -0
  362. reconcile/rhidp/sso_client/__init__.py +0 -0
  363. reconcile/rhidp/sso_client/base.py +266 -0
  364. reconcile/rhidp/sso_client/integration.py +60 -0
  365. reconcile/rhidp/sso_client/metrics.py +39 -0
  366. reconcile/run_integration.py +293 -0
  367. reconcile/saas_auto_promotions_manager/integration.py +69 -24
  368. reconcile/saas_auto_promotions_manager/merge_request_manager/batcher.py +208 -0
  369. reconcile/saas_auto_promotions_manager/merge_request_manager/desired_state.py +28 -0
  370. reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request.py +3 -4
  371. reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager_v2.py +172 -0
  372. reconcile/saas_auto_promotions_manager/merge_request_manager/metrics.py +42 -0
  373. reconcile/saas_auto_promotions_manager/merge_request_manager/mr_parser.py +226 -0
  374. reconcile/saas_auto_promotions_manager/merge_request_manager/open_merge_requests.py +23 -0
  375. reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py +108 -32
  376. reconcile/saas_auto_promotions_manager/meta.py +4 -0
  377. reconcile/saas_auto_promotions_manager/publisher.py +32 -4
  378. reconcile/saas_auto_promotions_manager/s3_exporter.py +77 -0
  379. reconcile/saas_auto_promotions_manager/subscriber.py +110 -23
  380. reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py +48 -41
  381. reconcile/saas_file_validator.py +16 -6
  382. reconcile/sendgrid_teammates.py +27 -12
  383. reconcile/service_dependencies.py +0 -3
  384. reconcile/signalfx_endpoint_monitoring.py +2 -5
  385. reconcile/skupper_network/integration.py +10 -11
  386. reconcile/skupper_network/models.py +3 -5
  387. reconcile/skupper_network/reconciler.py +28 -35
  388. reconcile/skupper_network/site_controller.py +8 -8
  389. reconcile/slack_base.py +4 -7
  390. reconcile/slack_usergroups.py +249 -171
  391. reconcile/sql_query.py +324 -171
  392. reconcile/status.py +0 -1
  393. reconcile/status_board.py +275 -0
  394. reconcile/statuspage/__init__.py +0 -5
  395. reconcile/statuspage/atlassian.py +219 -80
  396. reconcile/statuspage/integration.py +9 -97
  397. reconcile/statuspage/integrations/__init__.py +0 -0
  398. reconcile/statuspage/integrations/components.py +77 -0
  399. reconcile/statuspage/integrations/maintenances.py +111 -0
  400. reconcile/statuspage/page.py +107 -72
  401. reconcile/statuspage/state.py +6 -11
  402. reconcile/statuspage/status.py +8 -12
  403. reconcile/templates/rosa-classic-cluster-creation.sh.j2 +60 -0
  404. reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +61 -0
  405. reconcile/templating/__init__.py +0 -0
  406. reconcile/templating/lib/__init__.py +0 -0
  407. reconcile/templating/lib/merge_request_manager.py +180 -0
  408. reconcile/templating/lib/model.py +20 -0
  409. reconcile/templating/lib/rendering.py +191 -0
  410. reconcile/templating/renderer.py +410 -0
  411. reconcile/templating/validator.py +153 -0
  412. reconcile/terraform_aws_route53.py +13 -10
  413. reconcile/terraform_cloudflare_dns.py +92 -122
  414. reconcile/terraform_cloudflare_resources.py +15 -13
  415. reconcile/terraform_cloudflare_users.py +27 -27
  416. reconcile/terraform_init/__init__.py +0 -0
  417. reconcile/terraform_init/integration.py +165 -0
  418. reconcile/terraform_init/merge_request.py +57 -0
  419. reconcile/terraform_init/merge_request_manager.py +102 -0
  420. reconcile/terraform_repo.py +403 -0
  421. reconcile/terraform_resources.py +266 -168
  422. reconcile/terraform_tgw_attachments.py +417 -167
  423. reconcile/terraform_users.py +40 -17
  424. reconcile/terraform_vpc_peerings.py +310 -142
  425. reconcile/terraform_vpc_resources/__init__.py +0 -0
  426. reconcile/terraform_vpc_resources/integration.py +220 -0
  427. reconcile/terraform_vpc_resources/merge_request.py +57 -0
  428. reconcile/terraform_vpc_resources/merge_request_manager.py +107 -0
  429. reconcile/typed_queries/alerting_services_settings.py +1 -2
  430. reconcile/typed_queries/app_interface_custom_messages.py +24 -0
  431. reconcile/typed_queries/app_interface_deadmanssnitch_settings.py +17 -0
  432. reconcile/typed_queries/app_interface_metrics_exporter/__init__.py +0 -0
  433. reconcile/typed_queries/app_interface_metrics_exporter/onboarding_status.py +13 -0
  434. reconcile/typed_queries/app_interface_repo_url.py +1 -2
  435. reconcile/typed_queries/app_interface_state_settings.py +1 -3
  436. reconcile/typed_queries/app_interface_vault_settings.py +1 -2
  437. reconcile/typed_queries/app_quay_repos_escalation_policies.py +14 -0
  438. reconcile/typed_queries/apps.py +11 -0
  439. reconcile/typed_queries/aws_vpc_requests.py +9 -0
  440. reconcile/typed_queries/aws_vpcs.py +12 -0
  441. reconcile/typed_queries/cloudflare.py +10 -0
  442. reconcile/typed_queries/clusters.py +7 -5
  443. reconcile/typed_queries/clusters_minimal.py +6 -5
  444. reconcile/typed_queries/clusters_with_dms.py +16 -0
  445. reconcile/typed_queries/cost_report/__init__.py +0 -0
  446. reconcile/typed_queries/cost_report/app_names.py +22 -0
  447. reconcile/typed_queries/cost_report/cost_namespaces.py +43 -0
  448. reconcile/typed_queries/cost_report/settings.py +15 -0
  449. reconcile/typed_queries/dynatrace.py +10 -0
  450. reconcile/typed_queries/dynatrace_environments.py +14 -0
  451. reconcile/typed_queries/dynatrace_token_provider_token_specs.py +14 -0
  452. reconcile/typed_queries/external_resources.py +46 -0
  453. reconcile/typed_queries/get_state_aws_account.py +20 -0
  454. reconcile/typed_queries/glitchtip.py +10 -0
  455. reconcile/typed_queries/jenkins.py +25 -0
  456. reconcile/typed_queries/jira.py +7 -0
  457. reconcile/typed_queries/jira_settings.py +16 -0
  458. reconcile/typed_queries/jiralert_settings.py +22 -0
  459. reconcile/typed_queries/ocm.py +8 -0
  460. reconcile/typed_queries/pagerduty_instances.py +2 -7
  461. reconcile/typed_queries/quay.py +23 -0
  462. reconcile/typed_queries/repos.py +20 -8
  463. reconcile/typed_queries/reserved_networks.py +12 -0
  464. reconcile/typed_queries/saas_files.py +221 -167
  465. reconcile/typed_queries/slack.py +7 -0
  466. reconcile/typed_queries/slo_documents.py +12 -0
  467. reconcile/typed_queries/status_board.py +58 -0
  468. reconcile/typed_queries/tekton_pipeline_providers.py +1 -2
  469. reconcile/typed_queries/terraform_namespaces.py +1 -2
  470. reconcile/typed_queries/terraform_tgw_attachments/__init__.py +0 -0
  471. reconcile/typed_queries/terraform_tgw_attachments/aws_accounts.py +16 -0
  472. reconcile/typed_queries/unleash.py +10 -0
  473. reconcile/typed_queries/users.py +11 -0
  474. reconcile/typed_queries/vault.py +10 -0
  475. reconcile/unleash_feature_toggles/__init__.py +0 -0
  476. reconcile/unleash_feature_toggles/integration.py +287 -0
  477. reconcile/utils/acs/__init__.py +0 -0
  478. reconcile/utils/acs/base.py +81 -0
  479. reconcile/utils/acs/notifiers.py +143 -0
  480. reconcile/utils/acs/policies.py +163 -0
  481. reconcile/utils/acs/rbac.py +277 -0
  482. reconcile/utils/aggregated_list.py +11 -9
  483. reconcile/utils/amtool.py +6 -4
  484. reconcile/utils/aws_api.py +279 -66
  485. reconcile/utils/aws_api_typed/__init__.py +0 -0
  486. reconcile/utils/aws_api_typed/account.py +23 -0
  487. reconcile/utils/aws_api_typed/api.py +273 -0
  488. reconcile/utils/aws_api_typed/dynamodb.py +16 -0
  489. reconcile/utils/aws_api_typed/iam.py +67 -0
  490. reconcile/utils/aws_api_typed/organization.py +152 -0
  491. reconcile/utils/aws_api_typed/s3.py +26 -0
  492. reconcile/utils/aws_api_typed/service_quotas.py +79 -0
  493. reconcile/utils/aws_api_typed/sts.py +36 -0
  494. reconcile/utils/aws_api_typed/support.py +79 -0
  495. reconcile/utils/aws_helper.py +42 -3
  496. reconcile/utils/batches.py +11 -0
  497. reconcile/utils/binary.py +7 -9
  498. reconcile/utils/cloud_resource_best_practice/__init__.py +0 -0
  499. reconcile/utils/cloud_resource_best_practice/aws_rds.py +66 -0
  500. reconcile/utils/clusterhealth/__init__.py +0 -0
  501. reconcile/utils/clusterhealth/providerbase.py +39 -0
  502. reconcile/utils/clusterhealth/telemeter.py +39 -0
  503. reconcile/utils/config.py +3 -4
  504. reconcile/utils/deadmanssnitch_api.py +86 -0
  505. reconcile/utils/differ.py +205 -0
  506. reconcile/utils/disabled_integrations.py +4 -6
  507. reconcile/utils/dynatrace/__init__.py +0 -0
  508. reconcile/utils/dynatrace/client.py +93 -0
  509. reconcile/utils/early_exit_cache.py +289 -0
  510. reconcile/utils/elasticsearch_exceptions.py +5 -0
  511. reconcile/utils/environ.py +2 -2
  512. reconcile/utils/exceptions.py +4 -0
  513. reconcile/utils/expiration.py +4 -8
  514. reconcile/utils/extended_early_exit.py +210 -0
  515. reconcile/utils/external_resource_spec.py +34 -12
  516. reconcile/utils/external_resources.py +48 -20
  517. reconcile/utils/filtering.py +16 -0
  518. reconcile/utils/git.py +49 -16
  519. reconcile/utils/github_api.py +10 -9
  520. reconcile/utils/gitlab_api.py +333 -190
  521. reconcile/utils/glitchtip/client.py +97 -100
  522. reconcile/utils/glitchtip/models.py +89 -11
  523. reconcile/utils/gql.py +157 -58
  524. reconcile/utils/grouping.py +17 -0
  525. reconcile/utils/helm.py +89 -18
  526. reconcile/utils/helpers.py +51 -0
  527. reconcile/utils/imap_client.py +5 -6
  528. reconcile/utils/internal_groups/__init__.py +0 -0
  529. reconcile/utils/internal_groups/client.py +160 -0
  530. reconcile/utils/internal_groups/models.py +71 -0
  531. reconcile/utils/jenkins_api.py +10 -34
  532. reconcile/utils/jinja2/__init__.py +0 -0
  533. reconcile/utils/{jinja2_ext.py → jinja2/extensions.py} +6 -4
  534. reconcile/utils/jinja2/filters.py +142 -0
  535. reconcile/utils/jinja2/utils.py +278 -0
  536. reconcile/utils/jira_client.py +165 -8
  537. reconcile/utils/jjb_client.py +47 -35
  538. reconcile/utils/jobcontroller/__init__.py +0 -0
  539. reconcile/utils/jobcontroller/controller.py +413 -0
  540. reconcile/utils/jobcontroller/models.py +195 -0
  541. reconcile/utils/jsonpath.py +4 -5
  542. reconcile/utils/jump_host.py +13 -12
  543. reconcile/utils/keycloak.py +106 -0
  544. reconcile/utils/ldap_client.py +35 -6
  545. reconcile/utils/lean_terraform_client.py +115 -6
  546. reconcile/utils/membershipsources/__init__.py +0 -0
  547. reconcile/utils/membershipsources/app_interface_resolver.py +60 -0
  548. reconcile/utils/membershipsources/models.py +91 -0
  549. reconcile/utils/membershipsources/resolver.py +110 -0
  550. reconcile/utils/merge_request_manager/__init__.py +0 -0
  551. reconcile/utils/merge_request_manager/merge_request_manager.py +99 -0
  552. reconcile/utils/merge_request_manager/parser.py +67 -0
  553. reconcile/utils/metrics.py +511 -1
  554. reconcile/utils/models.py +123 -0
  555. reconcile/utils/mr/README.md +198 -0
  556. reconcile/utils/mr/__init__.py +14 -10
  557. reconcile/utils/mr/app_interface_reporter.py +2 -2
  558. reconcile/utils/mr/aws_access.py +4 -4
  559. reconcile/utils/mr/base.py +51 -31
  560. reconcile/utils/mr/clusters_updates.py +10 -7
  561. reconcile/utils/mr/glitchtip_access_reporter.py +2 -4
  562. reconcile/utils/mr/labels.py +14 -1
  563. reconcile/utils/mr/notificator.py +1 -3
  564. reconcile/utils/mr/ocm_update_recommended_version.py +1 -2
  565. reconcile/utils/mr/ocm_upgrade_scheduler_org_updates.py +7 -3
  566. reconcile/utils/mr/promote_qontract.py +203 -0
  567. reconcile/utils/mr/user_maintenance.py +24 -4
  568. reconcile/utils/oauth2_backend_application_session.py +132 -0
  569. reconcile/utils/oc.py +194 -170
  570. reconcile/utils/oc_connection_parameters.py +40 -51
  571. reconcile/utils/oc_filters.py +11 -13
  572. reconcile/utils/oc_map.py +14 -35
  573. reconcile/utils/ocm/__init__.py +30 -1
  574. reconcile/utils/ocm/addons.py +228 -0
  575. reconcile/utils/ocm/base.py +618 -5
  576. reconcile/utils/ocm/cluster_groups.py +5 -56
  577. reconcile/utils/ocm/clusters.py +111 -99
  578. reconcile/utils/ocm/identity_providers.py +66 -0
  579. reconcile/utils/ocm/label_sources.py +75 -0
  580. reconcile/utils/ocm/labels.py +139 -54
  581. reconcile/utils/ocm/manifests.py +39 -0
  582. reconcile/utils/ocm/ocm.py +182 -928
  583. reconcile/utils/ocm/products.py +758 -0
  584. reconcile/utils/ocm/search_filters.py +20 -28
  585. reconcile/utils/ocm/service_log.py +32 -79
  586. reconcile/utils/ocm/sre_capability_labels.py +51 -0
  587. reconcile/utils/ocm/status_board.py +66 -0
  588. reconcile/utils/ocm/subscriptions.py +49 -59
  589. reconcile/utils/ocm/syncsets.py +39 -0
  590. reconcile/utils/ocm/upgrades.py +181 -0
  591. reconcile/utils/ocm_base_client.py +71 -36
  592. reconcile/utils/openshift_resource.py +113 -67
  593. reconcile/utils/output.py +18 -11
  594. reconcile/utils/pagerduty_api.py +16 -10
  595. reconcile/utils/parse_dhms_duration.py +13 -1
  596. reconcile/utils/prometheus.py +123 -0
  597. reconcile/utils/promotion_state.py +56 -19
  598. reconcile/utils/promtool.py +5 -8
  599. reconcile/utils/quay_api.py +13 -25
  600. reconcile/utils/raw_github_api.py +3 -5
  601. reconcile/utils/repo_owners.py +2 -8
  602. reconcile/utils/rest_api_base.py +126 -0
  603. reconcile/utils/rosa/__init__.py +0 -0
  604. reconcile/utils/rosa/rosa_cli.py +310 -0
  605. reconcile/utils/rosa/session.py +201 -0
  606. reconcile/utils/ruamel.py +16 -0
  607. reconcile/utils/runtime/__init__.py +0 -1
  608. reconcile/utils/runtime/desired_state_diff.py +9 -20
  609. reconcile/utils/runtime/environment.py +33 -8
  610. reconcile/utils/runtime/integration.py +28 -12
  611. reconcile/utils/runtime/meta.py +1 -3
  612. reconcile/utils/runtime/runner.py +8 -11
  613. reconcile/utils/runtime/sharding.py +93 -36
  614. reconcile/utils/saasherder/__init__.py +1 -1
  615. reconcile/utils/saasherder/interfaces.py +143 -138
  616. reconcile/utils/saasherder/models.py +201 -43
  617. reconcile/utils/saasherder/saasherder.py +508 -378
  618. reconcile/utils/secret_reader.py +22 -27
  619. reconcile/utils/semver_helper.py +15 -1
  620. reconcile/utils/slack_api.py +124 -36
  621. reconcile/utils/smtp_client.py +1 -2
  622. reconcile/utils/sqs_gateway.py +10 -6
  623. reconcile/utils/state.py +276 -127
  624. reconcile/utils/terraform/config_client.py +6 -7
  625. reconcile/utils/terraform_client.py +284 -125
  626. reconcile/utils/terrascript/cloudflare_client.py +38 -17
  627. reconcile/utils/terrascript/cloudflare_resources.py +67 -18
  628. reconcile/utils/terrascript/models.py +2 -3
  629. reconcile/utils/terrascript/resources.py +1 -2
  630. reconcile/utils/terrascript_aws_client.py +1292 -540
  631. reconcile/utils/three_way_diff_strategy.py +157 -0
  632. reconcile/utils/unleash/__init__.py +11 -0
  633. reconcile/utils/{unleash.py → unleash/client.py} +35 -29
  634. reconcile/utils/unleash/server.py +145 -0
  635. reconcile/utils/vault.py +42 -32
  636. reconcile/utils/vaultsecretref.py +2 -4
  637. reconcile/utils/vcs.py +250 -0
  638. reconcile/vault_replication.py +38 -31
  639. reconcile/vpc_peerings_validator.py +82 -13
  640. tools/app_interface_metrics_exporter.py +70 -0
  641. tools/app_interface_reporter.py +44 -157
  642. tools/cli_commands/container_images_report.py +154 -0
  643. tools/cli_commands/cost_report/__init__.py +0 -0
  644. tools/cli_commands/cost_report/aws.py +137 -0
  645. tools/cli_commands/cost_report/cost_management_api.py +155 -0
  646. tools/cli_commands/cost_report/model.py +49 -0
  647. tools/cli_commands/cost_report/openshift.py +166 -0
  648. tools/cli_commands/cost_report/openshift_cost_optimization.py +187 -0
  649. tools/cli_commands/cost_report/response.py +124 -0
  650. tools/cli_commands/cost_report/util.py +72 -0
  651. tools/cli_commands/cost_report/view.py +524 -0
  652. tools/cli_commands/erv2.py +620 -0
  653. tools/cli_commands/gpg_encrypt.py +5 -8
  654. tools/cli_commands/systems_and_tools.py +489 -0
  655. tools/glitchtip_access_revalidation.py +1 -1
  656. tools/qontract_cli.py +2301 -673
  657. tools/saas_metrics_exporter/__init__.py +0 -0
  658. tools/saas_metrics_exporter/commit_distance/__init__.py +0 -0
  659. tools/saas_metrics_exporter/commit_distance/channel.py +63 -0
  660. tools/saas_metrics_exporter/commit_distance/commit_distance.py +103 -0
  661. tools/saas_metrics_exporter/commit_distance/metrics.py +19 -0
  662. tools/saas_metrics_exporter/main.py +99 -0
  663. tools/saas_promotion_state/__init__.py +0 -0
  664. tools/saas_promotion_state/saas_promotion_state.py +105 -0
  665. tools/sd_app_sre_alert_report.py +145 -0
  666. tools/template_validation.py +107 -0
  667. e2e_tests/cli.py +0 -83
  668. e2e_tests/create_namespace.py +0 -43
  669. e2e_tests/dedicated_admin_rolebindings.py +0 -44
  670. e2e_tests/dedicated_admin_test_base.py +0 -39
  671. e2e_tests/default_network_policies.py +0 -47
  672. e2e_tests/default_project_labels.py +0 -52
  673. e2e_tests/network_policy_test_base.py +0 -17
  674. e2e_tests/test_base.py +0 -56
  675. qontract_reconcile-0.9.1rc298.dist-info/METADATA +0 -63
  676. qontract_reconcile-0.9.1rc298.dist-info/RECORD +0 -585
  677. qontract_reconcile-0.9.1rc298.dist-info/top_level.txt +0 -4
  678. reconcile/ecr_mirror.py +0 -152
  679. reconcile/github_scanner.py +0 -74
  680. reconcile/gitlab_integrations.py +0 -63
  681. reconcile/gql_definitions/ocm_oidc_idp/clusters.py +0 -195
  682. reconcile/gql_definitions/ocp_release_mirror/ocp_release_mirror.py +0 -287
  683. reconcile/integrations_validator.py +0 -18
  684. reconcile/jenkins_plugins.py +0 -129
  685. reconcile/kafka_clusters.py +0 -208
  686. reconcile/ocm_cluster_admin.py +0 -42
  687. reconcile/ocm_oidc_idp.py +0 -198
  688. reconcile/ocp_release_mirror.py +0 -373
  689. reconcile/prometheus_rules_tester_old.py +0 -436
  690. reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager.py +0 -279
  691. reconcile/saas_auto_promotions_manager/utils/vcs.py +0 -141
  692. reconcile/sentry_config.py +0 -613
  693. reconcile/sentry_helper.py +0 -69
  694. reconcile/test/conftest.py +0 -187
  695. reconcile/test/fixtures.py +0 -24
  696. reconcile/test/saas_auto_promotions_manager/conftest.py +0 -69
  697. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py +0 -110
  698. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/data_keys.py +0 -10
  699. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_housekeeping.py +0 -200
  700. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_merge_request_manager.py +0 -151
  701. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/conftest.py +0 -63
  702. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/data_keys.py +0 -4
  703. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_multiple_namespaces.py +0 -46
  704. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_single_namespace.py +0 -94
  705. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_single_target.py +0 -44
  706. reconcile/test/saas_auto_promotions_manager/subscriber/conftest.py +0 -74
  707. reconcile/test/saas_auto_promotions_manager/subscriber/data_keys.py +0 -11
  708. reconcile/test/saas_auto_promotions_manager/subscriber/test_content_hash.py +0 -155
  709. reconcile/test/saas_auto_promotions_manager/subscriber/test_diff.py +0 -173
  710. reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_channels_config_hash.py +0 -226
  711. reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_channels_moving_ref.py +0 -224
  712. reconcile/test/saas_auto_promotions_manager/subscriber/test_single_channel_with_single_publisher.py +0 -350
  713. reconcile/test/saas_auto_promotions_manager/test_integration_test.py +0 -129
  714. reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_multiple_publishers_for_single_channel.py +0 -70
  715. reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_use_target_config_hash.py +0 -63
  716. reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_with_auto_promote.py +0 -74
  717. reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_without_auto_promote.py +0 -65
  718. reconcile/test/test_aggregated_list.py +0 -237
  719. reconcile/test/test_amtool.py +0 -37
  720. reconcile/test/test_auto_promoter.py +0 -295
  721. reconcile/test/test_aws_ami_share.py +0 -68
  722. reconcile/test/test_aws_iam_keys.py +0 -70
  723. reconcile/test/test_aws_iam_password_reset.py +0 -35
  724. reconcile/test/test_aws_support_cases_sos.py +0 -23
  725. reconcile/test/test_checkpoint.py +0 -178
  726. reconcile/test/test_cli.py +0 -41
  727. reconcile/test/test_closedbox_endpoint_monitoring.py +0 -207
  728. reconcile/test/test_gabi_authorized_users.py +0 -72
  729. reconcile/test/test_github_org.py +0 -154
  730. reconcile/test/test_github_repo_invites.py +0 -123
  731. reconcile/test/test_gitlab_housekeeping.py +0 -88
  732. reconcile/test/test_gitlab_labeler.py +0 -129
  733. reconcile/test/test_gitlab_members.py +0 -283
  734. reconcile/test/test_instrumented_wrappers.py +0 -18
  735. reconcile/test/test_integrations_manager.py +0 -995
  736. reconcile/test/test_jenkins_worker_fleets.py +0 -55
  737. reconcile/test/test_jump_host.py +0 -117
  738. reconcile/test/test_ldap_users.py +0 -123
  739. reconcile/test/test_make.py +0 -28
  740. reconcile/test/test_ocm_additional_routers.py +0 -134
  741. reconcile/test/test_ocm_addons_upgrade_scheduler_org.py +0 -149
  742. reconcile/test/test_ocm_clusters.py +0 -598
  743. reconcile/test/test_ocm_clusters_manifest_updates.py +0 -89
  744. reconcile/test/test_ocm_oidc_idp.py +0 -315
  745. reconcile/test/test_ocm_update_recommended_version.py +0 -145
  746. reconcile/test/test_ocm_upgrade_scheduler.py +0 -614
  747. reconcile/test/test_ocm_upgrade_scheduler_org_updater.py +0 -129
  748. reconcile/test/test_openshift_base.py +0 -730
  749. reconcile/test/test_openshift_namespace_labels.py +0 -345
  750. reconcile/test/test_openshift_namespaces.py +0 -256
  751. reconcile/test/test_openshift_resource.py +0 -415
  752. reconcile/test/test_openshift_resources_base.py +0 -440
  753. reconcile/test/test_openshift_saas_deploy_change_tester.py +0 -310
  754. reconcile/test/test_openshift_tekton_resources.py +0 -253
  755. reconcile/test/test_openshift_upgrade_watcher.py +0 -146
  756. reconcile/test/test_prometheus_rules_tester.py +0 -151
  757. reconcile/test/test_prometheus_rules_tester_old.py +0 -77
  758. reconcile/test/test_quay_membership.py +0 -86
  759. reconcile/test/test_quay_mirror.py +0 -109
  760. reconcile/test/test_quay_mirror_org.py +0 -70
  761. reconcile/test/test_quay_repos.py +0 -59
  762. reconcile/test/test_queries.py +0 -53
  763. reconcile/test/test_repo_owners.py +0 -47
  764. reconcile/test/test_requests_sender.py +0 -139
  765. reconcile/test/test_saasherder.py +0 -1074
  766. reconcile/test/test_saasherder_allowed_secret_paths.py +0 -127
  767. reconcile/test/test_secret_reader.py +0 -153
  768. reconcile/test/test_slack_base.py +0 -185
  769. reconcile/test/test_slack_usergroups.py +0 -744
  770. reconcile/test/test_sql_query.py +0 -19
  771. reconcile/test/test_terraform_cloudflare_dns.py +0 -117
  772. reconcile/test/test_terraform_cloudflare_resources.py +0 -106
  773. reconcile/test/test_terraform_cloudflare_users.py +0 -749
  774. reconcile/test/test_terraform_resources.py +0 -257
  775. reconcile/test/test_terraform_tgw_attachments.py +0 -631
  776. reconcile/test/test_terraform_users.py +0 -57
  777. reconcile/test/test_terraform_vpc_peerings.py +0 -499
  778. reconcile/test/test_terraform_vpc_peerings_build_desired_state.py +0 -1061
  779. reconcile/test/test_unleash.py +0 -138
  780. reconcile/test/test_utils_aws_api.py +0 -240
  781. reconcile/test/test_utils_aws_helper.py +0 -80
  782. reconcile/test/test_utils_cluster_version_data.py +0 -177
  783. reconcile/test/test_utils_data_structures.py +0 -13
  784. reconcile/test/test_utils_disabled_integrations.py +0 -86
  785. reconcile/test/test_utils_expiration.py +0 -109
  786. reconcile/test/test_utils_external_resource_spec.py +0 -383
  787. reconcile/test/test_utils_external_resources.py +0 -247
  788. reconcile/test/test_utils_github_api.py +0 -73
  789. reconcile/test/test_utils_gitlab_api.py +0 -20
  790. reconcile/test/test_utils_gpg.py +0 -69
  791. reconcile/test/test_utils_gql.py +0 -81
  792. reconcile/test/test_utils_helm.py +0 -306
  793. reconcile/test/test_utils_helpers.py +0 -55
  794. reconcile/test/test_utils_imap_client.py +0 -65
  795. reconcile/test/test_utils_jjb_client.py +0 -52
  796. reconcile/test/test_utils_jsonpath.py +0 -286
  797. reconcile/test/test_utils_ldap_client.py +0 -51
  798. reconcile/test/test_utils_mr.py +0 -226
  799. reconcile/test/test_utils_mr_clusters_updates.py +0 -77
  800. reconcile/test/test_utils_oc.py +0 -984
  801. reconcile/test/test_utils_ocm.py +0 -110
  802. reconcile/test/test_utils_pagerduty_api.py +0 -251
  803. reconcile/test/test_utils_parse_dhms_duration.py +0 -34
  804. reconcile/test/test_utils_password_validator.py +0 -155
  805. reconcile/test/test_utils_quay_api.py +0 -86
  806. reconcile/test/test_utils_semver_helper.py +0 -19
  807. reconcile/test/test_utils_sharding.py +0 -56
  808. reconcile/test/test_utils_slack_api.py +0 -439
  809. reconcile/test/test_utils_smtp_client.py +0 -73
  810. reconcile/test/test_utils_state.py +0 -256
  811. reconcile/test/test_utils_terraform.py +0 -13
  812. reconcile/test/test_utils_terraform_client.py +0 -585
  813. reconcile/test/test_utils_terraform_config_client.py +0 -219
  814. reconcile/test/test_utils_terrascript_aws_client.py +0 -277
  815. reconcile/test/test_utils_terrascript_cloudflare_client.py +0 -597
  816. reconcile/test/test_utils_terrascript_cloudflare_resources.py +0 -26
  817. reconcile/test/test_vault_replication.py +0 -515
  818. reconcile/test/test_vault_utils.py +0 -47
  819. reconcile/test/test_version_bump.py +0 -18
  820. reconcile/test/test_vpc_peerings_validator.py +0 -103
  821. reconcile/test/test_wrong_region.py +0 -78
  822. reconcile/typed_queries/glitchtip_settings.py +0 -18
  823. reconcile/typed_queries/ocp_release_mirror.py +0 -11
  824. reconcile/unleash_watcher.py +0 -120
  825. reconcile/utils/git_secrets.py +0 -63
  826. reconcile/utils/mr/auto_promoter.py +0 -218
  827. reconcile/utils/sentry_client.py +0 -383
  828. release/test_version.py +0 -50
  829. release/version.py +0 -100
  830. tools/test/test_qontract_cli.py +0 -60
  831. tools/test/test_sre_checkpoints.py +0 -79
  832. /e2e_tests/__init__.py → /reconcile/aus/upgrades.py +0 -0
  833. /reconcile/{gql_definitions/ocp_release_mirror → aws_account_manager}/__init__.py +0 -0
  834. /reconcile/{test → aws_ami_cleanup}/__init__.py +0 -0
  835. /reconcile/{test/saas_auto_promotions_manager → aws_cloudwatch_log_retention}/__init__.py +0 -0
  836. /reconcile/{test/saas_auto_promotions_manager/merge_request_manager → aws_saml_idp}/__init__.py +0 -0
  837. /reconcile/{test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager → aws_saml_roles}/__init__.py +0 -0
  838. /reconcile/{test/saas_auto_promotions_manager/merge_request_manager/renderer → aws_version_sync}/__init__.py +0 -0
  839. /reconcile/{test/saas_auto_promotions_manager/subscriber → aws_version_sync/merge_request_manager}/__init__.py +0 -0
  840. /reconcile/{test/saas_auto_promotions_manager/utils → cluster_auth_rhidp}/__init__.py +0 -0
  841. /reconcile/{test/saas_auto_promotions_manager/utils/saas_files_inventory → dynatrace_token_provider}/__init__.py +0 -0
  842. {release → reconcile/endpoints_discovery}/__init__.py +0 -0
  843. {tools/test → reconcile/external_resources}/__init__.py +0 -0
@@ -5,22 +5,22 @@ import json
5
5
  import logging
6
6
  import os
7
7
  import re
8
- from collections import ChainMap
8
+ from collections import (
9
+ ChainMap,
10
+ defaultdict,
11
+ )
9
12
  from collections.abc import (
13
+ Generator,
10
14
  Iterable,
11
15
  Mapping,
12
16
  MutableMapping,
13
17
  Sequence,
14
18
  )
15
19
  from contextlib import suppress
20
+ from datetime import UTC, datetime, timedelta
16
21
  from types import TracebackType
17
- from typing import (
18
- Any,
19
- Generator,
20
- Optional,
21
- Type,
22
- Union,
23
- )
22
+ from typing import Any
23
+ from urllib.parse import urlparse
24
24
 
25
25
  import yaml
26
26
  from github import (
@@ -39,10 +39,10 @@ from sretoolbox.utils import (
39
39
 
40
40
  from reconcile.github_org import get_default_config
41
41
  from reconcile.status import RunningState
42
+ from reconcile.utils import helm
42
43
  from reconcile.utils.gitlab_api import GitLabApi
43
44
  from reconcile.utils.jenkins_api import JenkinsApi
44
45
  from reconcile.utils.jjb_client import JJB
45
- from reconcile.utils.mr.base import MRClient
46
46
  from reconcile.utils.oc import (
47
47
  OCLocal,
48
48
  StatusCodeError,
@@ -51,6 +51,7 @@ from reconcile.utils.openshift_resource import OpenshiftResource as OR
51
51
  from reconcile.utils.openshift_resource import (
52
52
  ResourceInventory,
53
53
  ResourceKeyExistsError,
54
+ ResourceNotManagedError,
54
55
  fully_qualified_kind,
55
56
  )
56
57
  from reconcile.utils.promotion_state import (
@@ -58,8 +59,6 @@ from reconcile.utils.promotion_state import (
58
59
  PromotionState,
59
60
  )
60
61
  from reconcile.utils.saasherder.interfaces import (
61
- HasParameters,
62
- HasSecretParameters,
63
62
  SaasFile,
64
63
  SaasParentSaasPromotion,
65
64
  SaasResourceTemplate,
@@ -69,6 +68,7 @@ from reconcile.utils.saasherder.interfaces import (
69
68
  SaasSecretParameters,
70
69
  )
71
70
  from reconcile.utils.saasherder.models import (
71
+ Channel,
72
72
  ImageAuth,
73
73
  Namespace,
74
74
  Promotion,
@@ -87,7 +87,8 @@ from reconcile.utils.state import State
87
87
  TARGET_CONFIG_HASH = "target_config_hash"
88
88
 
89
89
 
90
- UNIQUE_SAAS_FILE_ENV_COMBO_LEN = 50
90
+ UNIQUE_SAAS_FILE_ENV_COMBO_LEN = 56
91
+ REQUEST_TIMEOUT = 60
91
92
 
92
93
 
93
94
  def is_commit_sha(ref: str) -> bool:
@@ -95,7 +96,8 @@ def is_commit_sha(ref: str) -> bool:
95
96
  return bool(re.search(r"^[0-9a-f]{40}$", ref))
96
97
 
97
98
 
98
- RtRef = tuple[str, str, str]
99
+ # saas_name, resource_template_name, resource_template_url, target_uid
100
+ RtRef = tuple[str, str, str, str]
99
101
  Resource = dict[str, Any]
100
102
  Resources = list[Resource]
101
103
 
@@ -112,15 +114,17 @@ class SaasHerder: # pylint: disable=too-many-public-methods
112
114
  secret_reader: SecretReaderBase,
113
115
  hash_length: int,
114
116
  repo_url: str,
115
- gitlab: Optional[GitLabApi] = None,
116
- jenkins_map: Optional[dict[str, JenkinsApi]] = None,
117
- state: Optional[State] = None,
117
+ gitlab: GitLabApi | None = None,
118
+ jenkins_map: dict[str, JenkinsApi] | None = None,
119
+ state: State | None = None,
118
120
  validate: bool = False,
119
121
  include_trigger_trace: bool = False,
122
+ all_saas_files: Iterable[SaasFile] | None = None,
120
123
  ):
121
124
  self.error_registered = False
122
125
  self.saas_files = saas_files
123
126
  self.repo_urls = self._collect_repo_urls()
127
+ self.image_patterns = self._collect_image_patterns()
124
128
  self.resolve_templated_parameters(self.saas_files)
125
129
  if validate:
126
130
  self._validate_saas_files()
@@ -138,6 +142,10 @@ class SaasHerder: # pylint: disable=too-many-public-methods
138
142
  self.include_trigger_trace = include_trigger_trace
139
143
  self.state = state
140
144
  self._promotion_state = PromotionState(state=state) if state else None
145
+ self._channel_map = self._assemble_channels(saas_files=all_saas_files)
146
+ self.images: set[str] = set()
147
+ self.blocked_versions = self._collect_blocked_versions()
148
+ self.hotfix_versions = self._collect_hotfix_versions()
141
149
 
142
150
  # each namespace is in fact a target,
143
151
  # so we can use it to calculate.
@@ -151,15 +159,18 @@ class SaasHerder: # pylint: disable=too-many-public-methods
151
159
  self.compare = self._get_saas_file_feature_enabled("compare", default=True)
152
160
  self.publish_job_logs = self._get_saas_file_feature_enabled("publish_job_logs")
153
161
  self.cluster_admin = self._get_saas_file_feature_enabled("cluster_admin")
162
+ self.validate_planned_data = self._get_saas_file_feature_enabled(
163
+ "validate_planned_data", default=True
164
+ )
154
165
 
155
166
  def __enter__(self) -> "SaasHerder":
156
167
  return self
157
168
 
158
169
  def __exit__(
159
170
  self,
160
- exc_type: Optional[Type[BaseException]],
161
- exc_value: Optional[BaseException],
162
- traceback: Optional[TracebackType],
171
+ exc_type: type[BaseException] | None,
172
+ exc_value: BaseException | None,
173
+ traceback: TracebackType | None,
163
174
  ) -> None:
164
175
  self.cleanup()
165
176
 
@@ -189,8 +200,8 @@ class SaasHerder: # pylint: disable=too-many-public-methods
189
200
  yield (saas_file, resource_template, target)
190
201
 
191
202
  def _get_saas_file_feature_enabled(
192
- self, name: str, default: Optional[bool] = None
193
- ) -> Optional[bool]:
203
+ self, name: str, default: bool | None = None
204
+ ) -> bool | None:
194
205
  """Returns a bool indicating if a feature is enabled in a saas file,
195
206
  or a supplied default. Returns False if there are multiple
196
207
  saas files in the process.
@@ -231,26 +242,34 @@ class SaasHerder: # pylint: disable=too-many-public-methods
231
242
  f"secret parameter path '{sp.secret.path}' does not match any of allowedSecretParameterPaths"
232
243
  )
233
244
 
245
+ def _validate_target_in_app(
246
+ self, saas_file: SaasFile, target: SaasResourceTemplateTarget
247
+ ) -> None:
248
+ if saas_file.validate_targets_in_app:
249
+ valid_app_names = {saas_file.app.name}
250
+ if saas_file.app.parent_app:
251
+ valid_app_names.add(saas_file.app.parent_app.name)
252
+ if target.namespace.app.name not in valid_app_names:
253
+ logging.error(
254
+ f"[{saas_file.name}] all targets (i.e., namespaces that you deploy to) within the saas file '{saas_file.name}' must belong to app(s) {valid_app_names}. However, your saas file also deploys to namespace '{target.namespace.name}' which belongs to app '{target.namespace.app.name}'."
255
+ )
256
+ self.valid = False
257
+
234
258
  def _validate_saas_files(self) -> None:
235
259
  self.valid = True
236
260
  saas_file_name_path_map: dict[str, list[str]] = {}
237
261
  tkn_unique_pipelineruns: dict[str, str] = {}
238
262
 
239
- publications: dict[str, RtRef] = {}
240
- subscriptions: dict[str, list[RtRef]] = {}
263
+ publications: dict[str, set[RtRef]] = defaultdict(set)
264
+ subscriptions: dict[str, list[RtRef]] = defaultdict(list)
241
265
 
242
266
  for saas_file in self.saas_files:
243
267
  saas_file_name_path_map.setdefault(saas_file.name, [])
244
268
  saas_file_name_path_map[saas_file.name].append(saas_file.path)
245
269
 
246
- saas_file_owners = [
247
- u.org_username
248
- for r in saas_file.self_service_roles or []
249
- for u in list(r.users) + list(r.bots)
250
- ]
251
- if not saas_file_owners:
270
+ if not saas_file.app.self_service_roles:
252
271
  logging.error(
253
- f"saas file {saas_file.name} has no owners: {saas_file.path}"
272
+ f"app {saas_file.app.name} has no self-service roles (saas file {saas_file.name})"
254
273
  )
255
274
  self.valid = False
256
275
 
@@ -309,12 +328,17 @@ class SaasHerder: # pylint: disable=too-many-public-methods
309
328
  target.namespace.environment.secret_parameters or [],
310
329
  saas_file.allowed_secret_parameter_paths or [],
311
330
  )
331
+ self._validate_target_in_app(saas_file, target)
312
332
 
313
333
  if target.promotion:
314
334
  rt_ref = (
315
335
  saas_file.path,
316
336
  resource_template.name,
317
337
  resource_template.url,
338
+ target.uid(
339
+ parent_saas_file_name=saas_file.name,
340
+ parent_resource_template_name=resource_template.name,
341
+ ),
318
342
  )
319
343
 
320
344
  # Get publications and subscriptions for the target
@@ -395,7 +419,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
395
419
  self,
396
420
  rt_ref: RtRef,
397
421
  promotion: SaasResourceTemplateTargetPromotion,
398
- publications: MutableMapping[str, RtRef],
422
+ publications: MutableMapping[str, set[RtRef]],
399
423
  subscriptions: MutableMapping[str, list[RtRef]],
400
424
  ) -> None:
401
425
  """
@@ -403,23 +427,23 @@ class SaasHerder: # pylint: disable=too-many-public-methods
403
427
  It validates a publish channel is unique across all publish targets.
404
428
  """
405
429
  for channel in promotion.publish or []:
406
- if channel in publications:
430
+ if rt_ref in publications[channel]:
407
431
  self.valid = False
432
+ # This should never be possible theoretically ...
408
433
  logging.error(
409
- "saas file promotion publish channel"
410
- "is not unique: {}".format(channel)
434
+ f"Non-unique resource template reference {rt_ref} in "
435
+ f"channel {channel}"
411
436
  )
412
437
  continue
413
- publications[channel] = rt_ref
438
+ publications[channel].add(rt_ref)
414
439
 
415
440
  for channel in promotion.subscribe or []:
416
- subscriptions.setdefault(channel, [])
417
441
  subscriptions[channel].append(rt_ref)
418
442
 
419
443
  def _check_promotions_have_same_source(
420
444
  self,
421
445
  subscriptions: Mapping[str, list[RtRef]],
422
- publications: Mapping[str, RtRef],
446
+ publications: Mapping[str, set[RtRef]],
423
447
  ) -> None:
424
448
  """
425
449
  Function to check that a promotion has the same repository
@@ -427,52 +451,58 @@ class SaasHerder: # pylint: disable=too-many-public-methods
427
451
  """
428
452
 
429
453
  for sub_channel, sub_targets in subscriptions.items():
430
- pub_channel_ref = publications.get(sub_channel)
431
- if not pub_channel_ref:
432
- self.valid = False
433
- else:
434
- (pub_saas, pub_rt_name, pub_rt_url) = pub_channel_ref
435
-
436
- for sub_saas, sub_rt_name, sub_rt_url in sub_targets:
437
- if not pub_channel_ref:
454
+ pub_channel_refs = publications.get(sub_channel, set())
455
+ for sub_saas, sub_rt_name, sub_rt_url, _ in sub_targets:
456
+ if not pub_channel_refs:
457
+ self.valid = False
438
458
  logging.error(
439
459
  "Channel is not published by any target\n"
440
- "subscriber_saas: {}\n"
441
- "subscriber_rt: {}\n"
442
- "channel: {}".format(sub_saas, sub_rt_name, sub_channel)
460
+ f"subscriber_saas: {sub_saas}\n"
461
+ f"subscriber_rt: {sub_rt_name}\n"
462
+ f"channel: {sub_channel}"
443
463
  )
444
- else:
464
+ for pub_ref in pub_channel_refs:
465
+ (pub_saas, pub_rt_name, pub_rt_url, _) = pub_ref
445
466
  if sub_rt_url != pub_rt_url:
446
467
  self.valid = False
447
468
  logging.error(
448
469
  "Subscriber and Publisher targets have different "
449
470
  "source repositories\n"
450
- "publisher_saas: {}\n"
451
- "publisher_rt: {}\n"
452
- "publisher_repo: {}\n"
453
- "subscriber_saas: {}\n"
454
- "subscriber_rt: {}\n"
455
- "subscriber_repo: {}\n".format(
456
- pub_saas,
457
- pub_rt_name,
458
- pub_rt_url,
459
- sub_saas,
460
- sub_rt_name,
461
- sub_rt_url,
462
- )
471
+ f"publisher_saas: {pub_saas}\n"
472
+ f"publisher_rt: {pub_rt_name}\n"
473
+ f"publisher_repo: {pub_rt_url}\n"
474
+ f"subscriber_saas: {sub_saas}\n"
475
+ f"subscriber_rt: {sub_rt_name}\n"
476
+ f"subscriber_repo: {sub_rt_url}\n"
463
477
  )
464
478
 
479
+ @staticmethod
480
+ def build_saas_file_env_combo(
481
+ saas_file_name: str,
482
+ env_name: str,
483
+ ) -> tuple[str, str]:
484
+ """
485
+ Build a tuple of short and long names for a saas file and environment combo,
486
+ max tekton pipelinerun name length can be 63,
487
+ leaving 7 for the rerun leaves us with 56 to create a unique pipelinerun name.
488
+
489
+ :param saas_file_name: name of the saas file
490
+ :param env_name: name of the environment
491
+ :return: (tkn_name, tkn_long_name)
492
+ """
493
+ tkn_long_name = f"{saas_file_name}-{env_name}"
494
+ tkn_name = tkn_long_name[:UNIQUE_SAAS_FILE_ENV_COMBO_LEN].rstrip("-")
495
+ return tkn_name, tkn_long_name
496
+
465
497
  def _check_saas_file_env_combo_unique(
466
498
  self,
467
499
  saas_file_name: str,
468
500
  env_name: str,
469
501
  tkn_unique_pipelineruns: Mapping[str, str],
470
502
  ) -> tuple[str, str]:
471
- # max tekton pipelinerun name length can be 63.
472
- # leaving 12 for the timestamp leaves us with 51
473
- # to create a unique pipelinerun name
474
- tkn_long_name = f"{saas_file_name}-{env_name}"
475
- tkn_name = tkn_long_name[:UNIQUE_SAAS_FILE_ENV_COMBO_LEN]
503
+ tkn_name, tkn_long_name = self.build_saas_file_env_combo(
504
+ saas_file_name, env_name
505
+ )
476
506
  if (
477
507
  tkn_name in tkn_unique_pipelineruns
478
508
  and tkn_unique_pipelineruns[tkn_name] != tkn_long_name
@@ -663,43 +693,24 @@ class SaasHerder: # pylint: disable=too-many-public-methods
663
693
  environment=target.namespace.environment,
664
694
  app=target.namespace.app,
665
695
  cluster=target.namespace.cluster,
666
- # managedResourceTypes is defined per saas_file
667
- # add it to each namespace in the current saas_file
696
+ # managedResourceTypes and managedResourceNames are defined per saas_file
697
+ # add them to each namespace in the current saas_file
668
698
  managed_resource_types=saas_file.managed_resource_types,
699
+ managed_resource_names=saas_file.managed_resource_names,
669
700
  )
670
701
  )
671
702
  return namespaces
672
703
 
673
704
  def _collect_repo_urls(self) -> set[str]:
674
- return set(
705
+ return {
675
706
  rt.url
676
707
  for saas_file in self.saas_files
677
708
  for rt in saas_file.resource_templates
678
- )
679
-
680
- @staticmethod
681
- def _collect_parameters(container: HasParameters) -> dict[str, str]:
682
- parameters = container.parameters or {}
683
- if isinstance(parameters, str):
684
- parameters = json.loads(parameters)
685
- # adjust Python's True/False
686
- for k, v in parameters.items():
687
- if v is True:
688
- parameters[k] = "true"
689
- elif v is False:
690
- parameters[k] = "false"
691
- elif any(isinstance(v, t) for t in [dict, list, tuple]):
692
- parameters[k] = json.dumps(v)
693
- return parameters
694
-
695
- def _collect_secret_parameters(
696
- self, container: HasSecretParameters
697
- ) -> dict[str, str]:
698
- return {
699
- sp.name: self.secret_reader.read_secret(sp.secret)
700
- for sp in container.secret_parameters or []
701
709
  }
702
710
 
711
+ def _collect_image_patterns(self) -> set[str]:
712
+ return {p for sf in self.saas_files for p in sf.image_patterns}
713
+
703
714
  @staticmethod
704
715
  def _get_file_contents_github(repo: Repository, path: str, commit_sha: str) -> str:
705
716
  f = repo.get_contents(path, commit_sha)
@@ -718,11 +729,31 @@ class SaasHerder: # pylint: disable=too-many-public-methods
718
729
 
719
730
  return ""
720
731
 
732
+ @retry(max_attempts=20)
733
+ def get_archive_info(
734
+ self,
735
+ saas_file: SaasFile,
736
+ trigger_reason: str,
737
+ ) -> tuple[str, str]:
738
+ [url, sha] = trigger_reason.split(" ")[0].split("/commit/")
739
+ repo_name = urlparse(url).path.strip("/")
740
+ file_name = f"{repo_name.replace('/', '-')}-{sha}.tar.gz"
741
+ if "github" in url:
742
+ github = self._initiate_github(saas_file, base_url="https://api.github.com")
743
+ repo = github.get_repo(repo_name)
744
+ # get_archive_link get redirect url form header, it does not work with github-mirror
745
+ archive_url = repo.get_archive_link("tarball", ref=sha)
746
+ elif "gitlab" in url:
747
+ archive_url = f"{url}/-/archive/{sha}/{file_name}"
748
+ else:
749
+ raise Exception(f"Only GitHub and GitLab are supported: {url}")
750
+
751
+ return file_name, archive_url
752
+
721
753
  @retry(max_attempts=20)
722
754
  def _get_file_contents(
723
755
  self, url: str, path: str, ref: str, github: Github
724
- ) -> tuple[Any, str, str]:
725
- html_url = f"{url}/blob/{ref}{path}"
756
+ ) -> tuple[Any, str]:
726
757
  commit_sha = self._get_commit_sha(url, ref, github)
727
758
 
728
759
  if "github" in url:
@@ -738,15 +769,14 @@ class SaasHerder: # pylint: disable=too-many-public-methods
738
769
  else:
739
770
  raise Exception(f"Only GitHub and GitLab are supported: {url}")
740
771
 
741
- return yaml.safe_load(content), html_url, commit_sha
772
+ return yaml.safe_load(content), commit_sha
742
773
 
743
774
  @retry()
744
775
  def _get_directory_contents(
745
776
  self, url: str, path: str, ref: str, github: Github
746
- ) -> tuple[list[Any], str, str]:
747
- html_url = f"{url}/tree/{ref}{path}"
777
+ ) -> tuple[list[Any], str]:
748
778
  commit_sha = self._get_commit_sha(url, ref, github)
749
- resources = []
779
+ resources: list[Any] = []
750
780
  if "github" in url:
751
781
  repo_name = url.rstrip("/").replace("https://github.com/", "")
752
782
  repo = github.get_repo(repo_name)
@@ -758,8 +788,8 @@ class SaasHerder: # pylint: disable=too-many-public-methods
758
788
  file_contents_decoded = self._get_file_contents_github(
759
789
  repo, file_path, commit_sha
760
790
  )
761
- resource = yaml.safe_load(file_contents_decoded)
762
- resources.append(resource)
791
+ result_resources = yaml.safe_load_all(file_contents_decoded)
792
+ resources.extend(result_resources)
763
793
  elif "gitlab" in url:
764
794
  if not self.gitlab:
765
795
  raise Exception("gitlab is not initialized")
@@ -775,7 +805,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
775
805
  else:
776
806
  raise Exception(f"Only GitHub and GitLab are supported: {url}")
777
807
 
778
- return resources, html_url, commit_sha
808
+ return resources, commit_sha
779
809
 
780
810
  @retry()
781
811
  def _get_commit_sha(self, url: str, ref: str, github: Github) -> str:
@@ -789,7 +819,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
789
819
  if not self.gitlab:
790
820
  raise Exception("gitlab is not initialized")
791
821
  project = self.gitlab.get_project(url)
792
- commits = project.commits.list(ref_name=ref)
822
+ commits = project.commits.list(ref_name=ref, per_page=1, page=1)
793
823
  commit_sha = commits[0].id
794
824
 
795
825
  return commit_sha
@@ -838,60 +868,32 @@ class SaasHerder: # pylint: disable=too-many-public-methods
838
868
  return True
839
869
  return False
840
870
 
841
- def _process_template(
842
- self,
843
- saas_file_name: str,
844
- resource_template_name: str,
845
- image_auth: ImageAuth,
846
- url: str,
847
- path: str,
848
- provider: str,
849
- hash_length: int,
850
- target: SaasResourceTemplateTarget,
851
- parameters: dict[str, str],
852
- github: Github,
853
- target_config_hash: str,
854
- ) -> tuple[list[Any], str, Optional[Promotion]]:
855
- if provider == "openshift-template":
856
- environment_parameters = self._collect_parameters(
857
- target.namespace.environment
858
- )
859
- environment_secret_parameters = self._collect_secret_parameters(
860
- target.namespace.environment
861
- )
862
- target_parameters = self._collect_parameters(target)
863
- target_secret_parameters = self._collect_secret_parameters(target)
864
-
865
- consolidated_parameters = {}
866
- consolidated_parameters.update(environment_parameters)
867
- consolidated_parameters.update(environment_secret_parameters)
868
- consolidated_parameters.update(parameters)
869
- consolidated_parameters.update(target_parameters)
870
- consolidated_parameters.update(target_secret_parameters)
871
-
872
- for replace_key, replace_value in consolidated_parameters.items():
873
- if not isinstance(replace_value, str):
874
- continue
875
- replace_pattern = "${" + replace_key + "}"
876
- for k, v in consolidated_parameters.items():
877
- if not isinstance(v, str):
878
- continue
879
- if replace_pattern in v:
880
- consolidated_parameters[k] = v.replace(
881
- replace_pattern, replace_value
882
- )
871
+ def _process_template(self, spec: TargetSpec) -> tuple[list[Any], Promotion | None]:
872
+ saas_file_name = spec.saas_file_name
873
+ resource_template_name = spec.resource_template_name
874
+ url = spec.url
875
+ path = spec.path
876
+ ref = spec.ref
877
+ provider = spec.provider
878
+ hash_length = spec.hash_length
879
+ target = spec.target
880
+ github = spec.github
881
+ target_config_hash = spec.target_config_hash
882
+ error_prefix = spec.error_prefix
883
883
 
884
+ if provider == "openshift-template":
885
+ consolidated_parameters = spec.parameters()
884
886
  try:
885
- template, html_url, commit_sha = self._get_file_contents(
886
- url=url, path=path, ref=target.ref, github=github
887
+ template, commit_sha = self._get_file_contents(
888
+ url=url, path=path, ref=ref, github=github
887
889
  )
888
890
  except Exception as e:
889
- logging.error(
890
- f"[{url}/blob/{target.ref}{path}] "
891
- + f"error fetching template: {str(e)}"
892
- )
891
+ logging.error(f"{error_prefix} error fetching template: {e!s}")
893
892
  raise
894
893
 
894
+ # add COMMIT_SHA only if it is unspecified
895
+ consolidated_parameters.setdefault("COMMIT_SHA", commit_sha)
896
+
895
897
  # add IMAGE_TAG only if it is unspecified
896
898
  if not (image_tag := consolidated_parameters.get("IMAGE_TAG", "")):
897
899
  sha_substring = commit_sha[:hash_length]
@@ -904,8 +906,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
904
906
  channel = consolidated_parameters["CHANNEL"]
905
907
  except KeyError:
906
908
  logging.error(
907
- f"[{saas_file_name}/{resource_template_name}] "
908
- + f"{html_url}: CHANNEL is required when "
909
+ f"{error_prefix} CHANNEL is required when "
909
910
  + "'use_channel_in_image_tag' is true."
910
911
  )
911
912
  raise
@@ -927,71 +928,129 @@ class SaasHerder: # pylint: disable=too-many-public-methods
927
928
  registry_image = consolidated_parameters["REGISTRY_IMG"]
928
929
  except KeyError as e:
929
930
  logging.error(
930
- f"[{saas_file_name}/{resource_template_name}] "
931
- + f"{html_url}: error generating REPO_DIGEST. "
931
+ f"{error_prefix} error generating REPO_DIGEST. "
932
932
  + "Is REGISTRY_IMG missing? "
933
- + f"{str(e)}"
934
- )
935
- raise
936
- try:
937
- image_uri = f"{registry_image}:{image_tag}"
938
- img = Image(
939
- url=image_uri,
940
- username=image_auth.username,
941
- password=image_auth.password,
942
- auth_server=image_auth.auth_server,
943
- )
944
- if need_repo_digest:
945
- consolidated_parameters["REPO_DIGEST"] = img.url_digest
946
- if need_image_digest:
947
- consolidated_parameters["IMAGE_DIGEST"] = img.digest
948
- except (rqexc.ConnectionError, rqexc.HTTPError) as e:
949
- logging.error(
950
- f"[{saas_file_name}/{resource_template_name}] "
951
- + f"{html_url}: error generating REPO_DIGEST for "
952
- + f"{image_uri}: {str(e)}"
933
+ + f"{e!s}"
953
934
  )
954
935
  raise
955
936
 
937
+ image_uri = f"{registry_image}:{image_tag}"
938
+ img = self._get_image(
939
+ image=image_uri,
940
+ image_patterns=spec.image_patterns,
941
+ image_auth=spec.image_auth,
942
+ error_prefix=error_prefix,
943
+ )
944
+ if not img:
945
+ msg = f"{error_prefix} error get image for {image_uri}"
946
+ logging.error(msg)
947
+ raise Exception(msg)
948
+
949
+ if need_repo_digest:
950
+ consolidated_parameters["REPO_DIGEST"] = img.url_digest
951
+ if need_image_digest:
952
+ consolidated_parameters["IMAGE_DIGEST"] = img.digest
953
+
956
954
  oc = OCLocal("cluster", None, None, local=True)
957
955
  try:
958
956
  resources = oc.process(template, consolidated_parameters)
959
957
  except StatusCodeError as e:
960
- logging.error(
961
- f"[{saas_file_name}/{resource_template_name}] "
962
- + f"{html_url}: error processing template: {str(e)}"
963
- )
958
+ logging.error(f"{error_prefix} error processing template: {e!s}")
964
959
 
965
960
  elif provider == "directory":
966
961
  try:
967
- resources, html_url, commit_sha = self._get_directory_contents(
968
- url=url, path=path, ref=target.ref, github=github
962
+ resources, commit_sha = self._get_directory_contents(
963
+ url=url, path=path, ref=ref, github=github
969
964
  )
970
965
  except Exception as e:
971
966
  logging.error(
972
- f"[{url}/tree/{target.ref}{path}] "
973
- + f"error fetching directory: {str(e)}"
967
+ f"{error_prefix} error fetching directory: {e!s} "
968
+ + "(We do not support nested directories. Do you by chance have subdirectories?)"
974
969
  )
975
970
  raise
976
971
 
977
- else:
978
- logging.error(
979
- f"[{saas_file_name}/{resource_template_name}] "
980
- + f"unknown provider: {provider}"
972
+ elif provider == "helm":
973
+ ssl_verify = (
974
+ self.gitlab.ssl_verify
975
+ if self.gitlab and url.startswith(self.gitlab.server)
976
+ else True
977
+ )
978
+ consolidated_parameters = spec.parameters(adjust=False)
979
+ resources = helm.template_all(
980
+ url=url,
981
+ path=path,
982
+ ref=ref,
983
+ namespace=spec.target.namespace.name,
984
+ values=consolidated_parameters,
985
+ ssl_verify=ssl_verify,
981
986
  )
982
987
 
988
+ else:
989
+ logging.error(f"{error_prefix} unknown provider: {provider}")
990
+
983
991
  target_promotion = None
984
992
  if target.promotion:
993
+ channels = [
994
+ self._channel_map[sub] for sub in target.promotion.subscribe or []
995
+ ]
985
996
  target_promotion = Promotion(
997
+ url=url,
986
998
  auto=target.promotion.auto,
987
999
  publish=target.promotion.publish,
988
- subscribe=target.promotion.subscribe,
1000
+ subscribe=channels,
989
1001
  promotion_data=target.promotion.promotion_data,
990
1002
  commit_sha=commit_sha,
991
1003
  saas_file=saas_file_name,
992
1004
  target_config_hash=target_config_hash,
1005
+ saas_target_uid=target.uid(
1006
+ parent_resource_template_name=resource_template_name,
1007
+ parent_saas_file_name=saas_file_name,
1008
+ ),
1009
+ soak_days=target.promotion.soak_days or 0,
993
1010
  )
994
- return resources, html_url, target_promotion
1011
+ return resources, target_promotion
1012
+
1013
+ def _assemble_channels(
1014
+ self, saas_files: Iterable[SaasFile] | None
1015
+ ) -> dict[str, Channel]:
1016
+ """
1017
+ We need to assemble all publisher_uids that are publishing to a channel.
1018
+ These uids are required to validate correctness of promotions.
1019
+ """
1020
+ channel_map: dict[str, Channel] = {}
1021
+ for saas_file in saas_files or []:
1022
+ for tmpl in saas_file.resource_templates:
1023
+ for target in tmpl.targets:
1024
+ if not target.promotion:
1025
+ continue
1026
+ for publish in target.promotion.publish or []:
1027
+ publisher_uid = target.uid(
1028
+ parent_saas_file_name=saas_file.name,
1029
+ parent_resource_template_name=tmpl.name,
1030
+ )
1031
+ if publish not in channel_map:
1032
+ channel_map[publish] = Channel(
1033
+ name=publish,
1034
+ publisher_uids=[],
1035
+ )
1036
+ channel_map[publish].publisher_uids.append(publisher_uid)
1037
+ return channel_map
1038
+
1039
+ def _collect_blocked_versions(self) -> dict[str, set[str]]:
1040
+ blocked_versions: dict[str, set[str]] = {}
1041
+ for saas_file in self.saas_files:
1042
+ for cc in saas_file.app.code_components or []:
1043
+ for v in cc.blocked_versions or []:
1044
+ blocked_versions.setdefault(cc.url, set()).add(v)
1045
+ return blocked_versions
1046
+
1047
+ def _collect_hotfix_versions(self) -> dict[str, set[str]]:
1048
+ hotfix_versions: dict[str, set[str]] = {}
1049
+ for saas_file in self.saas_files:
1050
+ for cc in saas_file.app.code_components or []:
1051
+ for v in cc.hotfix_versions or []:
1052
+ hotfix_versions.setdefault(cc.url, set()).add(v)
1053
+ return hotfix_versions
995
1054
 
996
1055
  @staticmethod
997
1056
  def _collect_images(resource: Resource) -> set[str]:
@@ -1028,79 +1087,114 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1028
1087
  return images
1029
1088
 
1030
1089
  @staticmethod
1031
- def _check_image(
1090
+ def _get_image(
1032
1091
  image: str,
1033
1092
  image_patterns: Iterable[str],
1034
1093
  image_auth: ImageAuth,
1035
1094
  error_prefix: str,
1036
- ) -> bool:
1037
- error = False
1095
+ ) -> Image | None:
1038
1096
  if not image_patterns:
1039
- error = True
1040
1097
  logging.error(
1041
1098
  f"{error_prefix} imagePatterns is empty (does not contain {image})"
1042
1099
  )
1100
+ return None
1043
1101
  if image_patterns and not any(image.startswith(p) for p in image_patterns):
1044
- error = True
1045
1102
  logging.error(f"{error_prefix} Image is not in imagePatterns: {image}")
1103
+ return None
1104
+
1105
+ # .dockerconfigjson
1106
+ if image_auth.docker_config:
1107
+ # we rely on the secret in vault being ordered
1108
+ # https://peps.python.org/pep-0468/
1109
+ for registry, auth in image_auth.docker_config["auths"].items():
1110
+ if not image.startswith(registry):
1111
+ continue
1112
+ username, password = (
1113
+ base64.b64decode(auth["auth"]).decode("utf-8").split(":")
1114
+ )
1115
+
1116
+ return SaasHerder._get_and_validate_image(
1117
+ full_image_path=image,
1118
+ username=username,
1119
+ password=password,
1120
+ auth_server=image_auth.auth_server,
1121
+ timeout=REQUEST_TIMEOUT,
1122
+ error_prefix=error_prefix,
1123
+ )
1124
+
1125
+ # basic auth fallback for backwards compatibility
1126
+ return SaasHerder._get_and_validate_image(
1127
+ full_image_path=image,
1128
+ username=image_auth.username,
1129
+ password=image_auth.password,
1130
+ auth_server=image_auth.auth_server,
1131
+ timeout=REQUEST_TIMEOUT,
1132
+ error_prefix=error_prefix,
1133
+ )
1134
+
1135
+ @staticmethod
1136
+ def _get_and_validate_image(
1137
+ full_image_path: str,
1138
+ username: str | Any,
1139
+ password: str | Any,
1140
+ auth_server: str | Any,
1141
+ timeout: int,
1142
+ error_prefix: str,
1143
+ ) -> Image | None:
1046
1144
  try:
1047
- valid = Image(
1048
- image,
1049
- username=image_auth.username,
1050
- password=image_auth.password,
1051
- auth_server=image_auth.auth_server,
1145
+ img = Image(
1146
+ full_image_path,
1147
+ username=username,
1148
+ password=password,
1149
+ auth_server=auth_server,
1150
+ timeout=timeout,
1052
1151
  )
1053
- if not valid:
1054
- error = True
1055
- logging.error(f"{error_prefix} Image does not exist: {image}")
1152
+ if img:
1153
+ return img
1154
+ else:
1155
+ logging.error(
1156
+ f"{error_prefix} Image : {full_image_path} does not exist"
1157
+ )
1056
1158
  except Exception as e:
1057
- error = True
1058
1159
  logging.error(
1059
- f"{error_prefix} Image is invalid: {image}. " + f"details: {str(e)}"
1160
+ f"{error_prefix} Image is invalid: {full_image_path}. "
1161
+ + f"details: {e!s}"
1060
1162
  )
1061
-
1062
- return error
1163
+ return None
1063
1164
 
1064
1165
  def _check_images(
1065
1166
  self,
1066
- saas_file_name: str,
1067
- resource_template_name: str,
1068
- image_auth: ImageAuth,
1069
- image_patterns: list[str],
1070
- html_url: str,
1167
+ spec: TargetSpec,
1071
1168
  resources: Resources,
1072
1169
  ) -> bool:
1073
- error_prefix = f"[{saas_file_name}/{resource_template_name}] {html_url}:"
1074
1170
  images_list = threaded.run(
1075
1171
  self._collect_images, resources, self.available_thread_pool_size
1076
1172
  )
1077
1173
  images = set(itertools.chain.from_iterable(images_list))
1174
+ self.images.update(images)
1078
1175
  if not images:
1079
1176
  return False # no errors
1080
- errors = threaded.run(
1081
- self._check_image,
1177
+ images = threaded.run(
1178
+ self._get_image,
1082
1179
  images,
1083
1180
  self.available_thread_pool_size,
1084
- image_patterns=image_patterns,
1085
- image_auth=image_auth,
1086
- error_prefix=error_prefix,
1181
+ image_patterns=spec.image_patterns,
1182
+ image_auth=spec.image_auth,
1183
+ error_prefix=spec.error_prefix,
1087
1184
  )
1088
- return any(errors)
1185
+ return None in images
1089
1186
 
1090
- def _initiate_github(self, saas_file: SaasFile) -> Github:
1187
+ def _initiate_github(
1188
+ self, saas_file: SaasFile, base_url: str | None = None
1189
+ ) -> Github:
1091
1190
  token = (
1092
1191
  self.secret_reader.read_secret(saas_file.authentication.code)
1093
1192
  if saas_file.authentication and saas_file.authentication.code
1094
1193
  else get_default_config()["token"]
1095
1194
  )
1096
-
1097
- base_url = os.environ.get("GITHUB_API", "https://api.github.com")
1098
- # This is a threaded world. Let's define a big
1099
- # connections pool to live in that world
1100
- # (this avoids the warning "Connection pool is
1101
- # full, discarding connection: api.github.com")
1102
- pool_size = 100
1103
- return Github(token, base_url=base_url, pool_size=pool_size)
1195
+ if not base_url:
1196
+ base_url = os.environ.get("GITHUB_API", "https://api.github.com")
1197
+ return Github(token, base_url=base_url)
1104
1198
 
1105
1199
  def _initiate_image_auth(self, saas_file: SaasFile) -> ImageAuth:
1106
1200
  """
@@ -1121,20 +1215,24 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1121
1215
  return ImageAuth()
1122
1216
 
1123
1217
  creds = self.secret_reader.read_all_secret(saas_file.authentication.image)
1124
- required_keys = ["user", "token"]
1125
- ok = all(k in creds.keys() for k in required_keys)
1218
+ required_docker_config_keys = [".dockerconfigjson"]
1219
+ required_keys_basic_auth = ["user", "token"]
1220
+ ok = all(k in creds for k in required_keys_basic_auth) or all(
1221
+ k in creds for k in required_docker_config_keys
1222
+ )
1126
1223
  if not ok:
1127
1224
  logging.warning(
1128
1225
  "the specified image authentication secret "
1129
1226
  + f"found in path {saas_file.authentication.image.path} "
1130
- + f"does not contain all required keys: {required_keys}"
1227
+ + f"does not contain all required keys: {required_docker_config_keys} or {required_keys_basic_auth}"
1131
1228
  )
1132
1229
  return ImageAuth()
1133
1230
 
1134
1231
  return ImageAuth(
1135
- username=creds["user"],
1136
- password=creds["token"],
1232
+ username=creds.get("user"),
1233
+ password=creds.get("token"),
1137
1234
  auth_server=creds.get("url"),
1235
+ docker_config=json.loads(creds.get(".dockerconfigjson") or "{}"),
1138
1236
  )
1139
1237
 
1140
1238
  def populate_desired_state(self, ri: ResourceInventory) -> None:
@@ -1150,7 +1248,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1150
1248
  self.thread_pool_size,
1151
1249
  ri=ri,
1152
1250
  )
1153
- self.promotions: list[Optional[Promotion]] = promotions
1251
+ self.promotions: list[Promotion | None] = promotions
1154
1252
 
1155
1253
  def _init_populate_desired_state_specs(
1156
1254
  self, saas_file: SaasFile
@@ -1158,23 +1256,10 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1158
1256
  specs = []
1159
1257
  github = self._initiate_github(saas_file)
1160
1258
  image_auth = self._initiate_image_auth(saas_file)
1161
- saas_file_parameters = self._collect_parameters(saas_file)
1162
- saas_file_secret_parameters = self._collect_secret_parameters(saas_file)
1163
-
1164
1259
  all_trigger_specs = self.get_saas_targets_config_trigger_specs(saas_file)
1165
1260
  # iterate over resource templates (multiple per saas_file)
1166
1261
  for rt in saas_file.resource_templates:
1167
- provider = rt.provider or "openshift-template"
1168
1262
  hash_length = rt.hash_length or self.hash_length
1169
- resource_template_parameters = self._collect_parameters(rt)
1170
- resource_template_secret_parameters = self._collect_secret_parameters(rt)
1171
-
1172
- consolidated_parameters = {}
1173
- consolidated_parameters.update(saas_file_parameters)
1174
- consolidated_parameters.update(saas_file_secret_parameters)
1175
- consolidated_parameters.update(resource_template_parameters)
1176
- consolidated_parameters.update(resource_template_secret_parameters)
1177
-
1178
1263
  # Iterate over targets (each target is a namespace).
1179
1264
  for target in rt.targets:
1180
1265
  if target.disable:
@@ -1198,25 +1283,15 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1198
1283
 
1199
1284
  specs.append(
1200
1285
  TargetSpec(
1201
- saas_file_name=saas_file.name,
1202
- cluster=target.namespace.cluster.name,
1203
- namespace=target.namespace.name,
1204
- managed_resource_types=saas_file.managed_resource_types,
1205
- delete=bool(target.delete),
1206
- privileged=bool(saas_file.cluster_admin),
1286
+ saas_file=saas_file,
1287
+ resource_template=rt,
1288
+ target=target,
1207
1289
  # process_template options
1208
- resource_template_name=rt.name,
1209
1290
  image_auth=image_auth,
1210
- url=rt.url,
1211
- path=rt.path,
1212
- provider=provider,
1213
1291
  hash_length=hash_length,
1214
- target=target,
1215
- parameters=consolidated_parameters,
1216
1292
  github=github,
1217
1293
  target_config_hash=digest,
1218
- # check_image options
1219
- image_patterns=saas_file.image_patterns,
1294
+ secret_reader=self.secret_reader,
1220
1295
  )
1221
1296
  )
1222
1297
 
@@ -1224,27 +1299,18 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1224
1299
 
1225
1300
  def populate_desired_state_saas_file(
1226
1301
  self, spec: TargetSpec, ri: ResourceInventory
1227
- ) -> Optional[Promotion]:
1302
+ ) -> Promotion | None:
1228
1303
  if spec.delete:
1229
1304
  # to delete resources, we avoid adding them to the desired state
1230
1305
  return None
1231
1306
 
1307
+ html_url = spec.html_url
1232
1308
  try:
1233
- resources, html_url, promotion = self._process_template(
1234
- saas_file_name=spec.saas_file_name,
1235
- resource_template_name=spec.resource_template_name,
1236
- image_auth=spec.image_auth,
1237
- url=spec.url,
1238
- path=spec.path,
1239
- provider=spec.provider,
1240
- hash_length=spec.hash_length,
1241
- target=spec.target,
1242
- parameters=spec.parameters,
1243
- github=spec.github,
1244
- target_config_hash=spec.target_config_hash,
1245
- )
1246
- except Exception:
1247
- # log message send in _process_template
1309
+ resources, promotion = self._process_template(spec)
1310
+ except Exception as e:
1311
+ # error log message send in _process_template. We cannot just
1312
+ # register an error without logging as inventory errors don't have details.
1313
+ logging.error(f"Error in populate_desired_state_saas_file: {e}")
1248
1314
  ri.register_error()
1249
1315
  return None
1250
1316
 
@@ -1274,11 +1340,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1274
1340
  self._additional_resource_process(resources, html_url)
1275
1341
  # check images
1276
1342
  image_error = self._check_images(
1277
- saas_file_name=spec.saas_file_name,
1278
- resource_template_name=spec.resource_template_name,
1279
- image_auth=spec.image_auth,
1280
- image_patterns=spec.image_patterns,
1281
- html_url=html_url,
1343
+ spec=spec,
1282
1344
  resources=resources,
1283
1345
  )
1284
1346
  if image_error:
@@ -1310,18 +1372,25 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1310
1372
  + f"{spec.resource_template_name}."
1311
1373
  )
1312
1374
  logging.error(msg)
1375
+ except ResourceNotManagedError:
1376
+ msg = (
1377
+ f"[{spec.cluster}/{spec.namespace}] desired item "
1378
+ + f"not managed, skipping: {oc_resource.kind}/{oc_resource.name}. "
1379
+ + f"saas file name: {spec.saas_file_name}, "
1380
+ + "resource template name: "
1381
+ + f"{spec.resource_template_name}."
1382
+ )
1383
+ logging.info(msg)
1313
1384
 
1314
1385
  return promotion
1315
1386
 
1316
1387
  def get_diff(
1317
1388
  self, trigger_type: TriggerTypes, dry_run: bool
1318
1389
  ) -> tuple[
1319
- Union[
1320
- list[TriggerSpecConfig],
1321
- list[TriggerSpecMovingCommit],
1322
- list[TriggerSpecUpstreamJob],
1323
- list[TriggerSpecContainerImage],
1324
- ],
1390
+ list[TriggerSpecConfig]
1391
+ | list[TriggerSpecMovingCommit]
1392
+ | list[TriggerSpecUpstreamJob]
1393
+ | list[TriggerSpecContainerImage],
1325
1394
  bool,
1326
1395
  ]:
1327
1396
  if trigger_type == TriggerTypes.MOVING_COMMITS:
@@ -1562,10 +1631,10 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1562
1631
  image_uri = f"{image_registry}:{desired_image_tag}"
1563
1632
  image_auth = self._initiate_image_auth(saas_file)
1564
1633
  error_prefix = f"[{saas_file.name}/{rt.name}] {target.ref}:"
1565
- error = self._check_image(
1634
+ image = self._get_image(
1566
1635
  image_uri, saas_file.image_patterns, image_auth, error_prefix
1567
1636
  )
1568
- if error:
1637
+ if not image:
1569
1638
  continue
1570
1639
 
1571
1640
  trigger_spec = TriggerSpecContainerImage(
@@ -1580,7 +1649,9 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1580
1649
  state_content=desired_image_tag,
1581
1650
  )
1582
1651
  if self.include_trigger_trace:
1583
- trigger_spec.reason = image_uri
1652
+ trigger_spec.reason = (
1653
+ f"{rt.url}/commit/{commit_sha} build {image_uri}"
1654
+ )
1584
1655
  if not self.state:
1585
1656
  raise Exception("state is not initialized")
1586
1657
  current_image_tag = self.state.get(trigger_spec.state_key, None)
@@ -1613,7 +1684,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1613
1684
  return list(itertools.chain.from_iterable(results))
1614
1685
 
1615
1686
  @staticmethod
1616
- def remove_none_values(d: Optional[dict[Any, Any]]) -> dict[Any, Any]:
1687
+ def remove_none_values(d: dict[Any, Any] | None) -> dict[Any, Any]:
1617
1688
  if d is None:
1618
1689
  return {}
1619
1690
  new = {}
@@ -1647,6 +1718,13 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1647
1718
 
1648
1719
  if self.include_trigger_trace:
1649
1720
  trigger_spec.reason = f"{self.repo_url}/commit/{RunningState().commit}"
1721
+ # For now we count every saas config change as an auto-promotion
1722
+ # if the auto promotion field is enabled in the saas target.
1723
+ # Ideally, we check if there was an actual ref change in order
1724
+ # to reduce false-positives.
1725
+ promotion = trigger_spec.state_content.get("promotion")
1726
+ if promotion and promotion.get("auto", False):
1727
+ trigger_spec.reason += " [auto-promotion]"
1650
1728
  trigger_specs.append(trigger_spec)
1651
1729
  return trigger_specs
1652
1730
 
@@ -1690,9 +1768,9 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1690
1768
  )
1691
1769
 
1692
1770
  # add managed resource types to target config
1693
- desired_target_config[
1694
- "saas_file_managed_resource_types"
1695
- ] = saas_file.managed_resource_types
1771
+ desired_target_config["saas_file_managed_resource_types"] = (
1772
+ saas_file.managed_resource_types
1773
+ )
1696
1774
  desired_target_config["url"] = rt.url
1697
1775
  desired_target_config["path"] = rt.path
1698
1776
  # before the GQL classes are introduced, the parameters attribute
@@ -1702,6 +1780,17 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1702
1780
  if rt.parameters is not None
1703
1781
  else None
1704
1782
  )
1783
+
1784
+ # include secret parameters from resource template and saas file
1785
+ if rt.secret_parameters:
1786
+ desired_target_config["rt_secretparameters"] = [
1787
+ p.dict() for p in rt.secret_parameters
1788
+ ]
1789
+ if saas_file.secret_parameters:
1790
+ desired_target_config["saas_file_secretparameters"] = [
1791
+ p.dict() for p in saas_file.secret_parameters
1792
+ ]
1793
+
1705
1794
  # Convert to dict, ChainMap is not JSON serializable
1706
1795
  # desired_target_config needs to be serialized to generate
1707
1796
  # its config hash and to be stored in S3
@@ -1733,84 +1822,115 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1733
1822
  "cluster": {"name": True, "server_url": True},
1734
1823
  "app": {"name": True},
1735
1824
  },
1825
+ # TODO: add environment.parameters to the include list!?!?
1736
1826
  )
1737
1827
 
1738
- def validate_promotions(self) -> bool:
1739
- """
1740
- If there were promotion sections in the participating saas files
1741
- validate that the conditions are met."""
1828
+ def _validate_promotion(self, promotion: Promotion) -> bool:
1829
+ # Placing this check here to make mypy happy
1742
1830
  if not (self.state and self._promotion_state):
1743
1831
  raise Exception("state is not initialized")
1744
1832
 
1745
- for promotion in self.promotions:
1746
- if promotion is None:
1747
- continue
1748
- # validate that the commit sha being promoted
1749
- # was successfully published to the subscribed channel(s)
1750
- if promotion.subscribe:
1751
- for channel in promotion.subscribe:
1752
- info = self._promotion_state.get_promotion_data(
1753
- sha=promotion.commit_sha, channel=channel, local_lookup=False
1754
- )
1755
- if not (info and info.success):
1756
- logging.error(
1757
- f"Commit {promotion.commit_sha} was not "
1758
- + f"published with success to channel {channel}"
1759
- )
1760
- return False
1761
- state_config_hash = info.target_config_hash
1762
-
1763
- # This code supports current saas targets that does
1764
- # not have promotion_data yet
1765
- if not state_config_hash or not promotion.promotion_data:
1766
- logging.info(
1767
- "Promotion data is missing; rely on the success "
1768
- "state only"
1769
- )
1770
- return True
1771
-
1772
- # Validate the promotion_data section.
1773
- # Just validate parent_saas_config hash
1774
- # promotion_data type by now.
1775
- parent_saas_config = None
1776
- for pd in promotion.promotion_data:
1777
- if pd.channel == channel:
1778
- for data in pd.data or []:
1779
- if isinstance(data, SaasParentSaasPromotion):
1780
- parent_saas_config = data
1781
-
1782
- # This section might not exist due to a manual MR.
1783
- # Promotion shall continue if this data is missing.
1784
- # The parent at the same ref has succeed if this code
1785
- # is reached though.
1786
- if not parent_saas_config:
1787
- logging.info(
1788
- "Parent Saas config missing on target "
1789
- "rely on the success state only"
1790
- )
1791
- return True
1833
+ if not promotion.subscribe:
1834
+ return True
1792
1835
 
1793
- # Validate that the state config_hash set by the parent
1794
- # matches with the hash set in promotion_data
1795
- if parent_saas_config.target_config_hash == state_config_hash:
1796
- return True
1836
+ if promotion.commit_sha in self.blocked_versions.get(promotion.url, set()):
1837
+ logging.error(f"Commit {promotion.commit_sha} is blocked!")
1838
+ return False
1797
1839
 
1840
+ # hotfix must run before further gates are evaluated to override them
1841
+ if promotion.commit_sha in self.hotfix_versions.get(promotion.url, set()):
1842
+ return True
1843
+
1844
+ now = datetime.now(UTC)
1845
+ passed_soak_days = timedelta(days=0)
1846
+
1847
+ for channel in promotion.subscribe:
1848
+ config_hashes: set[str] = set()
1849
+ for target_uid in channel.publisher_uids:
1850
+ deployment = self._promotion_state.get_promotion_data(
1851
+ sha=promotion.commit_sha,
1852
+ channel=channel.name,
1853
+ target_uid=target_uid,
1854
+ pre_check_sha_exists=False,
1855
+ )
1856
+ if not (
1857
+ deployment and (deployment.success or deployment.has_succeeded_once)
1858
+ ):
1798
1859
  logging.error(
1799
- "Parent saas target has run with a newer "
1800
- "configuration and the same commit (ref). "
1801
- "Check if other MR exists for this target, "
1802
- f"or update {parent_saas_config.target_config_hash} "
1803
- f"to {state_config_hash} for channel {channel}"
1860
+ f"Commit {promotion.commit_sha} was not "
1861
+ + f"published with success to channel {channel.name}"
1804
1862
  )
1805
1863
  return False
1864
+ if check_in := deployment.check_in:
1865
+ passed_soak_days += now - datetime.fromisoformat(check_in)
1866
+ if deployment.target_config_hash:
1867
+ config_hashes.add(deployment.target_config_hash)
1868
+
1869
+ # This code supports current saas targets that does
1870
+ # not have promotion_data yet
1871
+ if not config_hashes or not promotion.promotion_data:
1872
+ logging.info(
1873
+ "Promotion data is missing; rely on the success " "state only"
1874
+ )
1875
+ continue
1876
+
1877
+ # Validate the promotion_data section.
1878
+ # Just validate parent_saas_config hash
1879
+ # promotion_data type by now.
1880
+ parent_saas_config = None
1881
+ for pd in promotion.promotion_data:
1882
+ if pd.channel == channel.name:
1883
+ for data in pd.data or []:
1884
+ if isinstance(data, SaasParentSaasPromotion):
1885
+ parent_saas_config = data
1886
+
1887
+ # This section might not exist due to a manual MR.
1888
+ # Promotion shall continue if this data is missing.
1889
+ # The parent at the same ref has succeed if this code
1890
+ # is reached though.
1891
+ if not parent_saas_config:
1892
+ logging.info(
1893
+ "Parent Saas config missing on target "
1894
+ "rely on the success state only"
1895
+ )
1896
+ continue
1897
+
1898
+ # Validate that the state config_hash set by the parent
1899
+ # matches with the hash set in promotion_data
1900
+ if parent_saas_config.target_config_hash in config_hashes:
1901
+ continue
1902
+
1903
+ logging.error(
1904
+ "Parent saas target has run with a newer "
1905
+ "configuration and the same commit (ref). "
1906
+ "Check if other MR exists for this target, "
1907
+ f"or update {parent_saas_config.target_config_hash} "
1908
+ f"to any in {config_hashes} for channel {channel.name}"
1909
+ )
1910
+ return False
1911
+
1912
+ if passed_soak_days < timedelta(days=promotion.soak_days):
1913
+ logging.error(
1914
+ f"SoakDays in publishers did not pass. So far accumulated soakDays is {passed_soak_days},"
1915
+ f"but we have a soakDays setting of {promotion.soak_days}. We cannot proceed with this promotion."
1916
+ )
1917
+ return False
1806
1918
  return True
1807
1919
 
1920
+ def validate_promotions(self) -> bool:
1921
+ """
1922
+ If there were promotion sections in the participating saas files
1923
+ validate that the conditions are met."""
1924
+ return all(
1925
+ self._validate_promotion(promotion)
1926
+ for promotion in self.promotions
1927
+ if promotion is not None
1928
+ )
1929
+
1808
1930
  def publish_promotions(
1809
1931
  self,
1810
1932
  success: bool,
1811
1933
  all_saas_files: Iterable[SaasFile],
1812
- mr_cli: MRClient,
1813
- auto_promote: bool = False,
1814
1934
  ) -> None:
1815
1935
  """
1816
1936
  If there were promotion sections in the participating saas file
@@ -1819,17 +1939,11 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1819
1939
  subscribe_saas_file_path_map,
1820
1940
  subscribe_target_path_map,
1821
1941
  ) = self._get_subscribe_path_map(all_saas_files, auto_only=True)
1822
- trigger_promotion = False
1823
-
1824
- if self.promotions and not auto_promote:
1825
- logging.info(
1826
- "Auto-promotions to next stages are disabled. "
1827
- "Promotions are being handled by SAPM."
1828
- )
1829
1942
 
1830
1943
  if not (self.state and self._promotion_state):
1831
1944
  raise Exception("state is not initialized")
1832
1945
 
1946
+ now = datetime.now(UTC)
1833
1947
  for promotion in self.promotions:
1834
1948
  if promotion is None:
1835
1949
  continue
@@ -1838,14 +1952,43 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1838
1952
  all_subscribed_saas_file_paths = set()
1839
1953
  all_subscribed_target_paths = set()
1840
1954
  for channel in promotion.publish:
1955
+ # make sure we keep some attributes on re-deployments of same ref
1956
+ has_succeeded_once = success
1957
+ current_state = self._promotion_state.get_promotion_data(
1958
+ sha=promotion.commit_sha,
1959
+ channel=channel,
1960
+ target_uid=promotion.saas_target_uid,
1961
+ use_cache=True,
1962
+ )
1963
+ if current_state and current_state.has_succeeded_once:
1964
+ has_succeeded_once = True
1965
+
1966
+ check_in = str(now)
1967
+ if (
1968
+ success
1969
+ and current_state
1970
+ and current_state.check_in
1971
+ and current_state.success
1972
+ ):
1973
+ # We want to avoid an override of the timestamp.
1974
+ # This can happen on re-deployments of the same ref.
1975
+ # We only re-use the check_in time if the previous
1976
+ # and current deployment was successful.
1977
+ # On unsuccessful deployments, we
1978
+ # update the check_in time to current time.
1979
+ check_in = current_state.check_in
1980
+
1841
1981
  # publish to state to pass promotion gate
1842
1982
  self._promotion_state.publish_promotion_data(
1843
1983
  sha=promotion.commit_sha,
1844
1984
  channel=channel,
1985
+ target_uid=promotion.saas_target_uid,
1845
1986
  data=PromotionData(
1846
1987
  saas_file=promotion.saas_file,
1847
1988
  success=success,
1848
1989
  target_config_hash=promotion.target_config_hash,
1990
+ has_succeeded_once=has_succeeded_once,
1991
+ check_in=check_in,
1849
1992
  ),
1850
1993
  )
1851
1994
  logging.info(
@@ -1869,19 +2012,6 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1869
2012
  promotion.saas_file_paths = list(all_subscribed_saas_file_paths)
1870
2013
  promotion.target_paths = list(all_subscribed_target_paths)
1871
2014
 
1872
- if auto_promote and (
1873
- all_subscribed_saas_file_paths or all_subscribed_target_paths
1874
- ):
1875
- trigger_promotion = True
1876
-
1877
- if success and trigger_promotion:
1878
- from reconcile.utils.mr.auto_promoter import (
1879
- AutoPromoter, # avoid circular import
1880
- )
1881
-
1882
- mr = AutoPromoter([p for p in self.promotions if p is not None])
1883
- mr.submit(cli=mr_cli)
1884
-
1885
2015
  @staticmethod
1886
2016
  def _get_subscribe_path_map(
1887
2017
  saas_files: Iterable[SaasFile], auto_only: bool = False
@@ -1917,8 +2047,8 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1917
2047
  @staticmethod
1918
2048
  def resolve_templated_parameters(saas_files: Iterable[SaasFile]) -> None:
1919
2049
  """Resolve templated target parameters in saas files."""
1920
- from reconcile.openshift_resources_base import (
1921
- compile_jinja2_template, # avoid circular import
2050
+ from reconcile.utils.jinja2.utils import ( # noqa: PLC0415 - # avoid circular import
2051
+ compile_jinja2_template,
1922
2052
  )
1923
2053
 
1924
2054
  for saas_file in saas_files: