qontract-reconcile 0.10.2.dev334__py3-none-any.whl → 0.10.2.dev439__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. {qontract_reconcile-0.10.2.dev334.dist-info → qontract_reconcile-0.10.2.dev439.dist-info}/METADATA +13 -12
  2. {qontract_reconcile-0.10.2.dev334.dist-info → qontract_reconcile-0.10.2.dev439.dist-info}/RECORD +366 -361
  3. reconcile/acs_rbac.py +2 -2
  4. reconcile/aus/advanced_upgrade_service.py +18 -12
  5. reconcile/aus/base.py +134 -32
  6. reconcile/aus/cluster_version_data.py +15 -5
  7. reconcile/aus/models.py +3 -1
  8. reconcile/aus/ocm_addons_upgrade_scheduler_org.py +1 -0
  9. reconcile/aus/ocm_upgrade_scheduler.py +8 -1
  10. reconcile/aus/ocm_upgrade_scheduler_org.py +20 -5
  11. reconcile/aus/version_gates/sts_version_gate_handler.py +54 -1
  12. reconcile/automated_actions/config/integration.py +16 -4
  13. reconcile/aws_account_manager/integration.py +8 -8
  14. reconcile/aws_account_manager/reconciler.py +3 -3
  15. reconcile/aws_ami_cleanup/integration.py +8 -12
  16. reconcile/aws_ami_share.py +69 -62
  17. reconcile/aws_cloudwatch_log_retention/integration.py +155 -126
  18. reconcile/aws_ecr_image_pull_secrets.py +3 -3
  19. reconcile/aws_iam_keys.py +1 -0
  20. reconcile/aws_saml_idp/integration.py +12 -4
  21. reconcile/aws_saml_roles/integration.py +32 -25
  22. reconcile/aws_version_sync/integration.py +6 -12
  23. reconcile/change_owners/bundle.py +3 -3
  24. reconcile/change_owners/change_log_tracking.py +3 -2
  25. reconcile/change_owners/change_owners.py +1 -1
  26. reconcile/change_owners/diff.py +2 -4
  27. reconcile/checkpoint.py +11 -3
  28. reconcile/cli.py +111 -18
  29. reconcile/dashdotdb_dora.py +5 -12
  30. reconcile/dashdotdb_slo.py +1 -1
  31. reconcile/database_access_manager.py +123 -117
  32. reconcile/dynatrace_token_provider/integration.py +1 -1
  33. reconcile/endpoints_discovery/integration.py +4 -1
  34. reconcile/endpoints_discovery/merge_request.py +1 -1
  35. reconcile/endpoints_discovery/merge_request_manager.py +9 -11
  36. reconcile/external_resources/factories.py +5 -12
  37. reconcile/external_resources/integration.py +1 -1
  38. reconcile/external_resources/manager.py +8 -5
  39. reconcile/external_resources/meta.py +0 -1
  40. reconcile/external_resources/metrics.py +1 -1
  41. reconcile/external_resources/model.py +20 -20
  42. reconcile/external_resources/reconciler.py +7 -4
  43. reconcile/external_resources/secrets_sync.py +8 -11
  44. reconcile/external_resources/state.py +26 -16
  45. reconcile/fleet_labeler/integration.py +1 -1
  46. reconcile/gabi_authorized_users.py +8 -5
  47. reconcile/gcp_image_mirror.py +2 -2
  48. reconcile/github_org.py +1 -1
  49. reconcile/github_owners.py +4 -0
  50. reconcile/gitlab_housekeeping.py +13 -15
  51. reconcile/gitlab_members.py +6 -12
  52. reconcile/gitlab_mr_sqs_consumer.py +2 -2
  53. reconcile/gitlab_owners.py +15 -11
  54. reconcile/gitlab_permissions.py +8 -12
  55. reconcile/glitchtip_project_alerts/integration.py +3 -1
  56. reconcile/gql_definitions/acs/acs_instances.py +10 -10
  57. reconcile/gql_definitions/acs/acs_policies.py +5 -5
  58. reconcile/gql_definitions/acs/acs_rbac.py +6 -6
  59. reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py +32 -32
  60. reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py +26 -26
  61. reconcile/gql_definitions/app_interface_metrics_exporter/onboarding_status.py +6 -7
  62. reconcile/gql_definitions/app_sre_tekton_access_revalidation/roles.py +5 -5
  63. reconcile/gql_definitions/app_sre_tekton_access_revalidation/users.py +5 -5
  64. reconcile/gql_definitions/automated_actions/instance.py +51 -12
  65. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +11 -11
  66. reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +20 -10
  67. reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +28 -68
  68. reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +20 -10
  69. reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +20 -10
  70. reconcile/gql_definitions/aws_saml_roles/roles.py +5 -5
  71. reconcile/gql_definitions/aws_version_sync/clusters.py +10 -10
  72. reconcile/gql_definitions/aws_version_sync/namespaces.py +5 -5
  73. reconcile/gql_definitions/change_owners/queries/change_types.py +5 -5
  74. reconcile/gql_definitions/change_owners/queries/self_service_roles.py +9 -9
  75. reconcile/gql_definitions/cluster_auth_rhidp/clusters.py +18 -18
  76. reconcile/gql_definitions/common/alerting_services_settings.py +9 -9
  77. reconcile/gql_definitions/common/app_code_component_repos.py +5 -5
  78. reconcile/gql_definitions/common/app_interface_custom_messages.py +5 -5
  79. reconcile/gql_definitions/common/app_interface_dms_settings.py +5 -5
  80. reconcile/gql_definitions/common/app_interface_repo_settings.py +5 -5
  81. reconcile/gql_definitions/common/app_interface_roles.py +120 -0
  82. reconcile/gql_definitions/common/app_interface_state_settings.py +10 -10
  83. reconcile/gql_definitions/common/app_interface_vault_settings.py +5 -5
  84. reconcile/gql_definitions/common/app_quay_repos_escalation_policies.py +5 -5
  85. reconcile/gql_definitions/common/apps.py +5 -5
  86. reconcile/gql_definitions/common/aws_vpc_requests.py +22 -9
  87. reconcile/gql_definitions/common/aws_vpcs.py +11 -11
  88. reconcile/gql_definitions/common/clusters.py +37 -35
  89. reconcile/gql_definitions/common/clusters_minimal.py +14 -14
  90. reconcile/gql_definitions/common/clusters_with_dms.py +6 -6
  91. reconcile/gql_definitions/common/clusters_with_peering.py +29 -30
  92. reconcile/gql_definitions/common/github_orgs.py +10 -10
  93. reconcile/gql_definitions/common/jira_settings.py +10 -10
  94. reconcile/gql_definitions/common/jiralert_settings.py +5 -5
  95. reconcile/gql_definitions/common/ldap_settings.py +5 -5
  96. reconcile/gql_definitions/common/namespaces.py +42 -44
  97. reconcile/gql_definitions/common/namespaces_minimal.py +15 -13
  98. reconcile/gql_definitions/common/ocm_env_telemeter.py +12 -12
  99. reconcile/gql_definitions/common/ocm_environments.py +19 -19
  100. reconcile/gql_definitions/common/pagerduty_instances.py +9 -9
  101. reconcile/gql_definitions/common/pgp_reencryption_settings.py +6 -6
  102. reconcile/gql_definitions/common/pipeline_providers.py +29 -29
  103. reconcile/gql_definitions/common/quay_instances.py +5 -5
  104. reconcile/gql_definitions/common/quay_orgs.py +5 -5
  105. reconcile/gql_definitions/common/reserved_networks.py +5 -5
  106. reconcile/gql_definitions/common/rhcs_provider_settings.py +5 -5
  107. reconcile/gql_definitions/common/saas_files.py +44 -44
  108. reconcile/gql_definitions/common/saas_target_namespaces.py +10 -10
  109. reconcile/gql_definitions/common/saasherder_settings.py +5 -5
  110. reconcile/gql_definitions/common/slack_workspaces.py +5 -5
  111. reconcile/gql_definitions/common/smtp_client_settings.py +19 -19
  112. reconcile/gql_definitions/common/state_aws_account.py +7 -8
  113. reconcile/gql_definitions/common/users.py +5 -5
  114. reconcile/gql_definitions/common/users_with_paths.py +5 -5
  115. reconcile/gql_definitions/cost_report/app_names.py +5 -5
  116. reconcile/gql_definitions/cost_report/cost_namespaces.py +5 -5
  117. reconcile/gql_definitions/cost_report/settings.py +9 -9
  118. reconcile/gql_definitions/dashdotdb_slo/slo_documents_query.py +43 -43
  119. reconcile/gql_definitions/dynatrace_token_provider/dynatrace_bootstrap_tokens.py +10 -10
  120. reconcile/gql_definitions/dynatrace_token_provider/token_specs.py +5 -5
  121. reconcile/gql_definitions/email_sender/apps.py +5 -5
  122. reconcile/gql_definitions/email_sender/emails.py +8 -8
  123. reconcile/gql_definitions/email_sender/users.py +6 -6
  124. reconcile/gql_definitions/endpoints_discovery/apps.py +10 -10
  125. reconcile/gql_definitions/external_resources/aws_accounts.py +9 -9
  126. reconcile/gql_definitions/external_resources/external_resources_modules.py +23 -23
  127. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +494 -410
  128. reconcile/gql_definitions/external_resources/external_resources_settings.py +28 -26
  129. reconcile/gql_definitions/external_resources/fragments/external_resources_module_overrides.py +5 -5
  130. reconcile/gql_definitions/fleet_labeler/fleet_labels.py +40 -40
  131. reconcile/gql_definitions/fragments/aus_organization.py +5 -5
  132. reconcile/gql_definitions/fragments/aws_account_common.py +7 -5
  133. reconcile/gql_definitions/fragments/aws_account_managed.py +5 -5
  134. reconcile/gql_definitions/fragments/aws_account_sso.py +5 -5
  135. reconcile/gql_definitions/fragments/aws_infra_management_account.py +5 -5
  136. reconcile/gql_definitions/fragments/{aws_vpc_request_subnet.py → aws_organization.py} +12 -8
  137. reconcile/gql_definitions/fragments/aws_vpc.py +5 -5
  138. reconcile/gql_definitions/fragments/aws_vpc_request.py +12 -5
  139. reconcile/gql_definitions/fragments/container_image_mirror.py +5 -5
  140. reconcile/gql_definitions/fragments/deploy_resources.py +5 -5
  141. reconcile/gql_definitions/fragments/disable.py +5 -5
  142. reconcile/gql_definitions/fragments/email_service.py +5 -5
  143. reconcile/gql_definitions/fragments/email_user.py +5 -5
  144. reconcile/gql_definitions/fragments/jumphost_common_fields.py +5 -5
  145. reconcile/gql_definitions/fragments/membership_source.py +5 -5
  146. reconcile/gql_definitions/fragments/minimal_ocm_organization.py +5 -5
  147. reconcile/gql_definitions/fragments/oc_connection_cluster.py +5 -5
  148. reconcile/gql_definitions/fragments/ocm_environment.py +5 -5
  149. reconcile/gql_definitions/fragments/pipeline_provider_retention.py +5 -5
  150. reconcile/gql_definitions/fragments/prometheus_instance.py +5 -5
  151. reconcile/gql_definitions/fragments/resource_limits_requirements.py +5 -5
  152. reconcile/gql_definitions/fragments/resource_requests_requirements.py +5 -5
  153. reconcile/gql_definitions/fragments/resource_values.py +5 -5
  154. reconcile/gql_definitions/fragments/saas_slo_document.py +5 -5
  155. reconcile/gql_definitions/fragments/saas_target_namespace.py +5 -5
  156. reconcile/gql_definitions/fragments/serviceaccount_token.py +5 -5
  157. reconcile/gql_definitions/fragments/terraform_state.py +5 -5
  158. reconcile/gql_definitions/fragments/upgrade_policy.py +5 -5
  159. reconcile/gql_definitions/fragments/user.py +5 -5
  160. reconcile/gql_definitions/fragments/vault_secret.py +5 -5
  161. reconcile/gql_definitions/gcp/gcp_docker_repos.py +9 -9
  162. reconcile/gql_definitions/gcp/gcp_projects.py +9 -9
  163. reconcile/gql_definitions/gitlab_members/gitlab_instances.py +9 -9
  164. reconcile/gql_definitions/gitlab_members/permissions.py +9 -9
  165. reconcile/gql_definitions/glitchtip/glitchtip_instance.py +9 -9
  166. reconcile/gql_definitions/glitchtip/glitchtip_project.py +11 -11
  167. reconcile/gql_definitions/glitchtip_project_alerts/glitchtip_project.py +9 -9
  168. reconcile/gql_definitions/integrations/integrations.py +48 -51
  169. reconcile/gql_definitions/introspection.json +3207 -1683
  170. reconcile/gql_definitions/jenkins_configs/jenkins_configs.py +11 -11
  171. reconcile/gql_definitions/jenkins_configs/jenkins_instances.py +10 -10
  172. reconcile/gql_definitions/jira/jira_servers.py +5 -5
  173. reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +14 -10
  174. reconcile/gql_definitions/jumphosts/jumphosts.py +13 -13
  175. reconcile/gql_definitions/ldap_groups/roles.py +5 -5
  176. reconcile/gql_definitions/ldap_groups/settings.py +9 -9
  177. reconcile/gql_definitions/maintenance/maintenances.py +5 -5
  178. reconcile/gql_definitions/membershipsources/roles.py +5 -5
  179. reconcile/gql_definitions/ocm_labels/clusters.py +18 -19
  180. reconcile/gql_definitions/ocm_labels/organizations.py +5 -5
  181. reconcile/gql_definitions/openshift_cluster_bots/clusters.py +22 -22
  182. reconcile/gql_definitions/openshift_groups/managed_groups.py +5 -5
  183. reconcile/gql_definitions/openshift_groups/managed_roles.py +6 -6
  184. reconcile/gql_definitions/openshift_serviceaccount_tokens/tokens.py +10 -10
  185. reconcile/gql_definitions/quay_membership/quay_membership.py +6 -6
  186. reconcile/gql_definitions/rhcs/certs.py +33 -87
  187. reconcile/gql_definitions/rhcs/openshift_resource_rhcs_cert.py +43 -0
  188. reconcile/gql_definitions/rhidp/organizations.py +18 -18
  189. reconcile/gql_definitions/service_dependencies/jenkins_instance_fragment.py +5 -5
  190. reconcile/gql_definitions/service_dependencies/service_dependencies.py +8 -8
  191. reconcile/gql_definitions/sharding/aws_accounts.py +10 -10
  192. reconcile/gql_definitions/sharding/ocm_organization.py +8 -8
  193. reconcile/gql_definitions/skupper_network/site_controller_template.py +5 -5
  194. reconcile/gql_definitions/skupper_network/skupper_networks.py +10 -10
  195. reconcile/gql_definitions/slack_usergroups/clusters.py +5 -5
  196. reconcile/gql_definitions/slack_usergroups/permissions.py +9 -9
  197. reconcile/gql_definitions/slack_usergroups/users.py +5 -5
  198. reconcile/gql_definitions/slo_documents/slo_documents.py +5 -5
  199. reconcile/gql_definitions/status_board/status_board.py +6 -7
  200. reconcile/gql_definitions/statuspage/statuspages.py +9 -9
  201. reconcile/gql_definitions/templating/template_collection.py +5 -5
  202. reconcile/gql_definitions/templating/templates.py +5 -5
  203. reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py +6 -6
  204. reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py +11 -11
  205. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_accounts.py +11 -11
  206. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_resources.py +20 -25
  207. reconcile/gql_definitions/terraform_cloudflare_users/app_interface_setting_cloudflare_and_vault.py +6 -6
  208. reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.py +12 -12
  209. reconcile/gql_definitions/terraform_init/aws_accounts.py +23 -9
  210. reconcile/gql_definitions/terraform_repo/terraform_repo.py +9 -9
  211. reconcile/gql_definitions/terraform_resources/database_access_manager.py +5 -5
  212. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +440 -407
  213. reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +23 -17
  214. reconcile/gql_definitions/unleash_feature_toggles/feature_toggles.py +9 -9
  215. reconcile/gql_definitions/vault_instances/vault_instances.py +61 -61
  216. reconcile/gql_definitions/vault_policies/vault_policies.py +11 -11
  217. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator.py +8 -8
  218. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator_peered_cluster_fragment.py +5 -5
  219. reconcile/integrations_manager.py +3 -3
  220. reconcile/jenkins_worker_fleets.py +10 -8
  221. reconcile/jira_permissions_validator.py +237 -122
  222. reconcile/ldap_groups/integration.py +1 -1
  223. reconcile/ocm/types.py +35 -56
  224. reconcile/ocm_aws_infrastructure_access.py +1 -1
  225. reconcile/ocm_clusters.py +4 -4
  226. reconcile/ocm_labels/integration.py +3 -2
  227. reconcile/ocm_machine_pools.py +33 -27
  228. reconcile/openshift_base.py +113 -5
  229. reconcile/openshift_cluster_bots.py +3 -2
  230. reconcile/openshift_namespace_labels.py +1 -1
  231. reconcile/openshift_namespaces.py +97 -101
  232. reconcile/openshift_resources_base.py +6 -2
  233. reconcile/openshift_rhcs_certs.py +74 -37
  234. reconcile/openshift_rolebindings.py +230 -130
  235. reconcile/openshift_saas_deploy.py +6 -7
  236. reconcile/openshift_saas_deploy_change_tester.py +9 -7
  237. reconcile/openshift_saas_deploy_trigger_cleaner.py +3 -5
  238. reconcile/openshift_serviceaccount_tokens.py +2 -2
  239. reconcile/openshift_upgrade_watcher.py +4 -4
  240. reconcile/openshift_users.py +5 -3
  241. reconcile/oum/labelset.py +5 -3
  242. reconcile/oum/models.py +1 -4
  243. reconcile/prometheus_rules_tester/integration.py +3 -3
  244. reconcile/quay_mirror.py +1 -1
  245. reconcile/queries.py +131 -0
  246. reconcile/rhidp/common.py +3 -5
  247. reconcile/rhidp/sso_client/base.py +16 -5
  248. reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py +1 -1
  249. reconcile/saas_auto_promotions_manager/subscriber.py +4 -3
  250. reconcile/skupper_network/integration.py +2 -2
  251. reconcile/slack_usergroups.py +35 -14
  252. reconcile/sql_query.py +1 -0
  253. reconcile/status_board.py +6 -6
  254. reconcile/statuspage/atlassian.py +7 -7
  255. reconcile/statuspage/integrations/maintenances.py +4 -3
  256. reconcile/statuspage/page.py +4 -9
  257. reconcile/statuspage/status.py +5 -8
  258. reconcile/templates/rosa-classic-cluster-creation.sh.j2 +5 -1
  259. reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +4 -1
  260. reconcile/templating/lib/merge_request_manager.py +2 -2
  261. reconcile/templating/lib/rendering.py +3 -3
  262. reconcile/templating/renderer.py +12 -13
  263. reconcile/terraform_aws_route53.py +7 -1
  264. reconcile/terraform_cloudflare_dns.py +3 -3
  265. reconcile/terraform_cloudflare_resources.py +5 -5
  266. reconcile/terraform_cloudflare_users.py +3 -2
  267. reconcile/terraform_init/integration.py +187 -23
  268. reconcile/terraform_repo.py +16 -12
  269. reconcile/terraform_resources.py +17 -7
  270. reconcile/terraform_tgw_attachments.py +27 -19
  271. reconcile/terraform_users.py +7 -0
  272. reconcile/terraform_vpc_peerings.py +14 -3
  273. reconcile/terraform_vpc_resources/integration.py +20 -8
  274. reconcile/typed_queries/app_interface_roles.py +10 -0
  275. reconcile/typed_queries/aws_account_tags.py +41 -0
  276. reconcile/typed_queries/cost_report/app_names.py +1 -1
  277. reconcile/typed_queries/cost_report/cost_namespaces.py +2 -2
  278. reconcile/typed_queries/saas_files.py +13 -13
  279. reconcile/typed_queries/status_board.py +2 -2
  280. reconcile/unleash_feature_toggles/integration.py +4 -2
  281. reconcile/utils/acs/base.py +6 -3
  282. reconcile/utils/acs/policies.py +2 -2
  283. reconcile/utils/aggregated_list.py +4 -3
  284. reconcile/utils/aws_api.py +51 -20
  285. reconcile/utils/aws_api_typed/api.py +38 -9
  286. reconcile/utils/aws_api_typed/cloudformation.py +149 -0
  287. reconcile/utils/aws_api_typed/logs.py +73 -0
  288. reconcile/utils/aws_api_typed/organization.py +4 -2
  289. reconcile/utils/binary.py +7 -12
  290. reconcile/utils/datetime_util.py +67 -0
  291. reconcile/utils/deadmanssnitch_api.py +1 -1
  292. reconcile/utils/differ.py +2 -3
  293. reconcile/utils/early_exit_cache.py +11 -12
  294. reconcile/utils/expiration.py +7 -3
  295. reconcile/utils/external_resource_spec.py +24 -1
  296. reconcile/utils/filtering.py +1 -1
  297. reconcile/utils/gitlab_api.py +7 -5
  298. reconcile/utils/glitchtip/client.py +6 -2
  299. reconcile/utils/glitchtip/models.py +25 -28
  300. reconcile/utils/gql.py +4 -7
  301. reconcile/utils/helm.py +2 -1
  302. reconcile/utils/helpers.py +1 -1
  303. reconcile/utils/instrumented_wrappers.py +1 -1
  304. reconcile/utils/internal_groups/client.py +2 -2
  305. reconcile/utils/internal_groups/models.py +8 -17
  306. reconcile/utils/jinja2/utils.py +6 -8
  307. reconcile/utils/jira_client.py +82 -63
  308. reconcile/utils/jjb_client.py +28 -15
  309. reconcile/utils/jobcontroller/controller.py +2 -2
  310. reconcile/utils/jobcontroller/models.py +17 -1
  311. reconcile/utils/json.py +74 -0
  312. reconcile/utils/membershipsources/app_interface_resolver.py +4 -2
  313. reconcile/utils/membershipsources/models.py +16 -23
  314. reconcile/utils/membershipsources/resolver.py +4 -2
  315. reconcile/utils/merge_request_manager/merge_request_manager.py +4 -4
  316. reconcile/utils/merge_request_manager/parser.py +6 -6
  317. reconcile/utils/metrics.py +5 -5
  318. reconcile/utils/models.py +304 -82
  319. reconcile/utils/mr/app_interface_reporter.py +2 -2
  320. reconcile/utils/mr/base.py +2 -2
  321. reconcile/utils/mr/notificator.py +3 -3
  322. reconcile/utils/mr/update_access_report_base.py +3 -4
  323. reconcile/utils/mr/user_maintenance.py +3 -2
  324. reconcile/utils/oc.py +249 -203
  325. reconcile/utils/oc_filters.py +3 -3
  326. reconcile/utils/ocm/addons.py +0 -1
  327. reconcile/utils/ocm/base.py +18 -21
  328. reconcile/utils/ocm/cluster_groups.py +1 -1
  329. reconcile/utils/ocm/identity_providers.py +2 -2
  330. reconcile/utils/ocm/labels.py +1 -1
  331. reconcile/utils/ocm/products.py +9 -3
  332. reconcile/utils/ocm/search_filters.py +3 -6
  333. reconcile/utils/ocm/service_log.py +4 -6
  334. reconcile/utils/ocm/sre_capability_labels.py +20 -13
  335. reconcile/utils/openshift_resource.py +10 -5
  336. reconcile/utils/output.py +3 -2
  337. reconcile/utils/pagerduty_api.py +10 -7
  338. reconcile/utils/promotion_state.py +6 -11
  339. reconcile/utils/raw_github_api.py +1 -1
  340. reconcile/utils/rhcsv2_certs.py +138 -35
  341. reconcile/utils/rosa/session.py +16 -0
  342. reconcile/utils/runtime/integration.py +2 -3
  343. reconcile/utils/runtime/runner.py +2 -2
  344. reconcile/utils/saasherder/interfaces.py +13 -20
  345. reconcile/utils/saasherder/models.py +25 -21
  346. reconcile/utils/saasherder/saasherder.py +55 -31
  347. reconcile/utils/slack_api.py +26 -4
  348. reconcile/utils/sloth.py +224 -0
  349. reconcile/utils/sqs_gateway.py +2 -1
  350. reconcile/utils/state.py +2 -1
  351. reconcile/utils/structs.py +1 -1
  352. reconcile/utils/terraform_client.py +5 -4
  353. reconcile/utils/terrascript_aws_client.py +192 -114
  354. reconcile/utils/unleash/server.py +2 -8
  355. reconcile/utils/vault.py +5 -12
  356. reconcile/utils/vcs.py +8 -8
  357. reconcile/vault_replication.py +107 -42
  358. tools/app_interface_reporter.py +4 -4
  359. tools/cli_commands/cost_report/cost_management_api.py +3 -3
  360. tools/cli_commands/cost_report/view.py +7 -6
  361. tools/cli_commands/erv2.py +1 -1
  362. tools/cli_commands/systems_and_tools.py +5 -1
  363. tools/qontract_cli.py +31 -18
  364. tools/template_validation.py +3 -1
  365. reconcile/gql_definitions/cna/__init__.py +0 -0
  366. reconcile/gql_definitions/cna/queries/__init__.py +0 -0
  367. reconcile/gql_definitions/ocm_oidc_idp/__init__.py +0 -0
  368. reconcile/gql_definitions/ocm_subscription_labels/__init__.py +0 -0
  369. {qontract_reconcile-0.10.2.dev334.dist-info → qontract_reconcile-0.10.2.dev439.dist-info}/WHEEL +0 -0
  370. {qontract_reconcile-0.10.2.dev334.dist-info → qontract_reconcile-0.10.2.dev439.dist-info}/entry_points.txt +0 -0
@@ -19,19 +19,17 @@ from reconcile.statuspage.status import (
19
19
  PROVIDER_NAME = "statuspage"
20
20
 
21
21
 
22
- class StatusComponent(BaseModel):
22
+ class StatusComponent(BaseModel, arbitrary_types_allowed=True):
23
23
  """
24
24
  Represents a status page component from the desired state.
25
25
  """
26
26
 
27
27
  name: str
28
28
  display_name: str
29
- description: str | None
30
- group_name: str | None
29
+ description: str | None = None
30
+ group_name: str | None = None
31
+ # Status provider configs hold different ways for a component to determine its status
31
32
  status_provider_configs: list[StatusProvider]
32
- """
33
- Status provider configs hold different ways for a component to determine its status
34
- """
35
33
 
36
34
  def status_management_enabled(self) -> bool:
37
35
  """
@@ -49,9 +47,6 @@ class StatusComponent(BaseModel):
49
47
  return "operational"
50
48
  return None
51
49
 
52
- class Config:
53
- arbitrary_types_allowed = True
54
-
55
50
  def __eq__(self, other: object) -> bool:
56
51
  if not isinstance(other, StatusComponent):
57
52
  raise NotImplementedError("Cannot compare to non StatusComponent objects.")
@@ -2,18 +2,15 @@ from abc import (
2
2
  ABC,
3
3
  abstractmethod,
4
4
  )
5
- from datetime import (
6
- UTC,
7
- datetime,
8
- )
5
+ from datetime import datetime
9
6
 
10
- from dateutil.parser import isoparse
11
7
  from pydantic import BaseModel
12
8
 
13
9
  from reconcile.gql_definitions.statuspage.statuspages import (
14
10
  ManualStatusProviderV1,
15
11
  StatusProviderV1,
16
12
  )
13
+ from reconcile.utils.datetime_util import from_utc_iso_format, utc_now
17
14
 
18
15
  # This module defines the interface for status providers for components on status
19
16
  # pages. A status provider is responsible for determining the status of a component.
@@ -70,7 +67,7 @@ class ManualStatusProvider(StatusProvider, BaseModel):
70
67
  raise ValueError(
71
68
  "manual component status time window is invalid: end before start"
72
69
  )
73
- now = datetime.now(UTC)
70
+ now = utc_now()
74
71
  if self.start and now < self.start:
75
72
  return False
76
73
  return not (self.end and self.end < now)
@@ -84,8 +81,8 @@ def build_status_provider_config(
84
81
  provider specific implementation that provides the status resolution logic.
85
82
  """
86
83
  if isinstance(cfg, ManualStatusProviderV1):
87
- start = isoparse(cfg.manual.q_from) if cfg.manual.q_from else None
88
- end = isoparse(cfg.manual.until) if cfg.manual.until else None
84
+ start = from_utc_iso_format(cfg.manual.q_from) if cfg.manual.q_from else None
85
+ end = from_utc_iso_format(cfg.manual.until) if cfg.manual.until else None
89
86
  return ManualStatusProvider(
90
87
  component_status=cfg.manual.component_status,
91
88
  start=start,
@@ -47,7 +47,7 @@ rosa create cluster -y --cluster-name={{ cluster_name }} \
47
47
  --service-cidr {{ cluster.network.service }} \
48
48
  --pod-cidr {{ cluster.network.pod }} \
49
49
  --host-prefix 23 \
50
- --replicas {{ cluster.machine_pools | length }} \
50
+ --replicas 3 \
51
51
  --compute-machine-type {{ cluster.machine_pools[0].instance_type }} \
52
52
  {% if cluster.spec.disable_user_workload_monitoring -%}
53
53
  --disable-workload-monitoring \
@@ -55,4 +55,8 @@ rosa create cluster -y --cluster-name={{ cluster_name }} \
55
55
  {% if cluster.spec.provision_shard_id -%}
56
56
  --properties provision_shard_id:{{ cluster.spec.provision_shard_id }} \
57
57
  {% endif -%}
58
+ {% if cluster.spec.fips -%}
59
+ --fips \
60
+ {% endif -%}
58
61
  --channel-group {{ cluster.spec.channel }}
62
+
@@ -47,7 +47,7 @@ rosa create cluster --cluster-name={{ cluster_name }} \
47
47
  --service-cidr {{ cluster.network.service }} \
48
48
  --pod-cidr {{ cluster.network.pod }} \
49
49
  --host-prefix 23 \
50
- --replicas {{ cluster.machine_pools | length }} \
50
+ --replicas 3 \
51
51
  --compute-machine-type {{ cluster.machine_pools[0].instance_type }} \
52
52
  {% if cluster.spec.private -%}
53
53
  --private \
@@ -59,4 +59,7 @@ rosa create cluster --cluster-name={{ cluster_name }} \
59
59
  {% if cluster.spec.provision_shard_id -%}
60
60
  --properties provision_shard_id:{{ cluster.spec.provision_shard_id }} \
61
61
  {% endif -%}
62
+ {% if cluster.spec.fips -%}
63
+ --fips \
64
+ {% endif -%}
62
65
  --channel-group {{ cluster.spec.channel }}
@@ -108,14 +108,14 @@ class TemplateRenderingMR(MergeRequestBase):
108
108
  if content.is_new:
109
109
  gitlab_cli.create_file(
110
110
  branch_name=self.branch,
111
- file_path=f"data{content.path}",
111
+ file_path=content.path.lstrip("/"),
112
112
  commit_message="termplate rendering output",
113
113
  content=content.content,
114
114
  )
115
115
  else:
116
116
  gitlab_cli.update_file(
117
117
  branch_name=self.branch,
118
- file_path=f"data{content.path}",
118
+ file_path=content.path.lstrip("/"),
119
119
  commit_message="termplate rendering output",
120
120
  content=content.content,
121
121
  )
@@ -18,7 +18,7 @@ from reconcile.utils.secret_reader import SecretReaderBase
18
18
 
19
19
  class TemplateData(BaseModel):
20
20
  variables: dict[str, Any]
21
- current: dict[str, Any] | None
21
+ current: dict[str, Any] | None = None
22
22
  current_with_explicit_start: bool | None = False
23
23
 
24
24
 
@@ -26,7 +26,7 @@ class TemplatePatch(Protocol):
26
26
  path: str
27
27
  identifier: str | None
28
28
 
29
- def dict(self) -> dict[str, str]: ...
29
+ def model_dump(self) -> dict[str, str]: ...
30
30
 
31
31
 
32
32
  class Template(Protocol):
@@ -36,7 +36,7 @@ class Template(Protocol):
36
36
  template: str
37
37
  overwrite: bool | None
38
38
 
39
- def dict(self) -> dict[str, str]: ...
39
+ def model_dump(self) -> dict[str, str]: ...
40
40
 
41
41
  @property
42
42
  def patch(self) -> TemplatePatch | None:
@@ -33,6 +33,7 @@ from reconcile.utils import gql
33
33
  from reconcile.utils.git import checkout, clone
34
34
  from reconcile.utils.gql import GqlApi
35
35
  from reconcile.utils.jinja2.utils import TemplateRenderOptions, process_jinja2_template
36
+ from reconcile.utils.json import json_dumps
36
37
  from reconcile.utils.ruamel import create_ruamel_instance
37
38
  from reconcile.utils.runtime.integration import (
38
39
  PydanticRunParams,
@@ -95,18 +96,16 @@ class LocalFilePersistence(FilePersistence):
95
96
  This class provides a simple file persistence implementation for local files.
96
97
  """
97
98
 
98
- def __init__(self, dry_run: bool, app_interface_data_path: str) -> None:
99
+ def __init__(self, dry_run: bool, app_interface_root_path: str) -> None:
99
100
  super().__init__(dry_run)
100
- if not app_interface_data_path.endswith("/data"):
101
- raise ValueError("app_interface_data_path should end with /data")
102
- self.app_interface_data_path = app_interface_data_path
101
+ self.app_interface_root_path = app_interface_root_path
103
102
 
104
103
  def read(self, path: str) -> str | None:
105
- return self._read_local_file(join_path(self.app_interface_data_path, path))
104
+ return self._read_local_file(join_path(self.app_interface_root_path, path))
106
105
 
107
106
  def flush(self) -> None:
108
107
  for output in self.outputs:
109
- filepath = Path(join_path(self.app_interface_data_path, output.path))
108
+ filepath = Path(join_path(self.app_interface_root_path, output.path))
110
109
  filepath.parent.mkdir(parents=True, exist_ok=True)
111
110
  filepath.write_text(output.content, encoding="utf-8")
112
111
 
@@ -159,7 +158,7 @@ class ClonedRepoGitlabPersistence(FilePersistence):
159
158
  self, dry_run: bool, local_path: str, vcs: VCS, mr_manager: MergeRequestManager
160
159
  ):
161
160
  super().__init__(dry_run)
162
- self.local_path = join_path(local_path, "data")
161
+ self.local_path = local_path
163
162
  self.vcs = vcs
164
163
  self.mr_manager = mr_manager
165
164
 
@@ -217,7 +216,7 @@ def unpack_static_variables(
217
216
  each: dict[str, Any],
218
217
  ) -> dict:
219
218
  return {
220
- k: json.loads(process_jinja2_template(body=json.dumps(v), vars={"each": each}))
219
+ k: json.loads(process_jinja2_template(body=json_dumps(v), vars={"each": each}))
221
220
  for k, v in (collection_variables.static or {}).items()
222
221
  }
223
222
 
@@ -240,8 +239,8 @@ def unpack_dynamic_variables(
240
239
 
241
240
  class TemplateRendererIntegrationParams(PydanticRunParams):
242
241
  clone_repo: bool = False
243
- app_interface_data_path: str | None
244
- template_collection_name: str | None
242
+ app_interface_root_path: str | None = None
243
+ template_collection_name: str | None = None
245
244
 
246
245
 
247
246
  def join_path(base: str, sub: str) -> str:
@@ -367,10 +366,10 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
367
366
  persistence: FilePersistence
368
367
  ruaml_instance = create_ruamel_instance(explicit_start=True)
369
368
 
370
- if not self.params.clone_repo and self.params.app_interface_data_path:
369
+ if not self.params.clone_repo and self.params.app_interface_root_path:
371
370
  persistence = LocalFilePersistence(
372
371
  dry_run=dry_run,
373
- app_interface_data_path=self.params.app_interface_data_path,
372
+ app_interface_root_path=self.params.app_interface_root_path,
374
373
  )
375
374
  self.reconcile(persistence, ruaml_instance)
376
375
 
@@ -411,4 +410,4 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
411
410
  self.reconcile(persistence, ruaml_instance)
412
411
 
413
412
  else:
414
- raise ValueError("App-interface-data-path must be set")
413
+ raise ValueError("App-interface-root-path must be set")
@@ -9,6 +9,7 @@ from typing import Any
9
9
 
10
10
  from reconcile import queries
11
11
  from reconcile.status import ExitCodes
12
+ from reconcile.typed_queries.external_resources import get_settings
12
13
  from reconcile.utils import dnsutils
13
14
  from reconcile.utils.aws_api import AWSApi
14
15
  from reconcile.utils.constants import DEFAULT_THREAD_POOL_SIZE
@@ -227,13 +228,18 @@ def run(
227
228
  f"No participating AWS accounts found, consider disabling this integration, account name: {account_name}"
228
229
  )
229
230
  return
230
-
231
+ try:
232
+ default_tags = get_settings().default_tags
233
+ except ValueError:
234
+ # no external resources settings found
235
+ default_tags = None
231
236
  ts = Terrascript(
232
237
  QONTRACT_INTEGRATION,
233
238
  "",
234
239
  thread_pool_size,
235
240
  participating_accounts,
236
241
  settings=settings,
242
+ default_tags=default_tags,
237
243
  )
238
244
 
239
245
  desired_state = build_desired_state(zones, all_accounts, settings)
@@ -155,7 +155,7 @@ class TerraformCloudflareDNSIntegration(
155
155
 
156
156
  accts_per_zone = []
157
157
  for zone in query_zones.zones or []:
158
- acct = zone.account.dict(by_alias=True)
158
+ acct = zone.account.model_dump(by_alias=True)
159
159
  acct["name"] = f"{zone.account.name}-{zone.identifier}"
160
160
  accts_per_zone.append(acct)
161
161
 
@@ -369,11 +369,11 @@ def cloudflare_dns_zone_to_external_resource(
369
369
  provision_provider=DEFAULT_PROVISIONER_PROVIDER,
370
370
  provisioner={"name": f"{zone.account.name}-{zone.identifier}"},
371
371
  namespace=DEFAULT_NAMESPACE,
372
- resource=zone.dict(by_alias=True, exclude=DEFAULT_EXCLUDE_KEY),
372
+ resource=zone.model_dump(by_alias=True, exclude=DEFAULT_EXCLUDE_KEY),
373
373
  )
374
374
  external_resource_spec.resource["provider"] = DEFAULT_PROVIDER
375
375
  external_resource_spec.resource["records"] = [
376
- record.dict(by_alias=True) for record in zone.records or []
376
+ record.model_dump(by_alias=True) for record in zone.records or []
377
377
  ]
378
378
  external_resource_specs.append(external_resource_spec)
379
379
  return external_resource_specs
@@ -168,7 +168,7 @@ def _build_oc_resources(
168
168
  internal=internal,
169
169
  )
170
170
 
171
- namespace_mapping = [ns.dict() for ns in cloudflare_namespaces]
171
+ namespace_mapping = [ns.model_dump() for ns in cloudflare_namespaces]
172
172
 
173
173
  state_specs = init_specs_to_fetch(
174
174
  ri, oc_map, namespaces=namespace_mapping, override_managed_types=["Secret"]
@@ -338,7 +338,7 @@ def run(
338
338
  )
339
339
 
340
340
  if not cloudflare_namespaces:
341
- logging.info("No cloudflare namespaces were detected, nothing to do.")
341
+ logging.debug("No cloudflare namespaces were detected, nothing to do.")
342
342
  sys.exit(ExitCodes.SUCCESS)
343
343
 
344
344
  # Build Cloudflare clients
@@ -351,7 +351,7 @@ def run(
351
351
  spec
352
352
  for namespace in query_resources.namespaces
353
353
  for spec in get_external_resource_specs(
354
- namespace.dict(by_alias=True), PROVIDER_CLOUDFLARE
354
+ namespace.model_dump(by_alias=True), PROVIDER_CLOUDFLARE
355
355
  )
356
356
  if not selected_account or spec.provisioner_name == selected_account
357
357
  ]
@@ -383,7 +383,7 @@ def run(
383
383
  QONTRACT_INTEGRATION_VERSION,
384
384
  QONTRACT_TF_PREFIX,
385
385
  [
386
- acct.dict(by_alias=True) # convert CloudflareAccountV1 to dict
386
+ acct.model_dump(by_alias=True) # convert CloudflareAccountV1 to dict
387
387
  for acct in query_accounts.accounts or []
388
388
  if acct.name in cf_clients.dump() # use only if it is a registered client
389
389
  ],
@@ -442,4 +442,4 @@ def _get_cloudflare_desired_state() -> tuple[
442
442
  def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
443
443
  desired_state = _get_cloudflare_desired_state()
444
444
 
445
- return {state.__repr_name__(): state.dict() for state in desired_state}
445
+ return {str(state): state.model_dump() for state in desired_state}
@@ -88,7 +88,7 @@ class TerraformCloudflareUsers(
88
88
  if not settings.settings:
89
89
  raise RuntimeError("App interface setting not defined")
90
90
 
91
- early_exit_desired_state = cloudflare_roles.dict()
91
+ early_exit_desired_state = cloudflare_roles.model_dump()
92
92
  early_exit_desired_state.update({
93
93
  CLOUDFLARE_EMAIL_DOMAIN_ALLOW_LIST_KEY: settings.settings
94
94
  })
@@ -149,7 +149,8 @@ class TerraformCloudflareUsers(
149
149
  }
150
150
 
151
151
  accounts = [
152
- acct.dict(by_alias=True) for _, acct in account_names_to_account.items()
152
+ acct.model_dump(by_alias=True)
153
+ for _, acct in account_names_to_account.items()
153
154
  ]
154
155
 
155
156
  self._run_terraform(
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  from collections.abc import Callable
3
- from datetime import UTC, datetime
4
3
  from typing import Any
5
4
 
6
5
  import jinja2
@@ -12,12 +11,16 @@ from reconcile.gql_definitions.terraform_init.aws_accounts import (
12
11
  from reconcile.terraform_init.merge_request import Renderer, create_parser
13
12
  from reconcile.terraform_init.merge_request_manager import MergeRequestManager, MrData
14
13
  from reconcile.typed_queries.app_interface_repo_url import get_app_interface_repo_url
14
+ from reconcile.typed_queries.aws_account_tags import get_aws_account_tags
15
+ from reconcile.typed_queries.external_resources import get_settings
15
16
  from reconcile.typed_queries.github_orgs import get_github_orgs
16
17
  from reconcile.typed_queries.gitlab_instances import get_gitlab_instances
17
18
  from reconcile.utils import gql
18
19
  from reconcile.utils.aws_api_typed.api import AWSApi, AWSStaticCredentials
20
+ from reconcile.utils.datetime_util import utc_now
19
21
  from reconcile.utils.defer import defer
20
22
  from reconcile.utils.disabled_integrations import integration_is_enabled
23
+ from reconcile.utils.gql import GqlApi
21
24
  from reconcile.utils.runtime.integration import (
22
25
  PydanticRunParams,
23
26
  QontractReconcileIntegration,
@@ -31,10 +34,16 @@ QONTRACT_INTEGRATION_VERSION = make_semver(1, 0, 0)
31
34
 
32
35
 
33
36
  class TerraformInitIntegrationParams(PydanticRunParams):
34
- account_name: str | None
37
+ account_name: str | None = None
35
38
  # To avoid the accidental deletion of the resource file, explicitly set the
36
39
  # qontract.cli option in the integration extraArgs!
37
40
  state_tmpl_resource: str = "/terraform-init/terraform-state.yml"
41
+ cloudformation_template_resource: str = (
42
+ "/terraform-init/terraform-state-s3-bucket.yaml"
43
+ )
44
+ cloudformation_import_template_resource: str = (
45
+ "/terraform-init/terraform-state-s3-bucket-import.yaml"
46
+ )
38
47
  template_collection_root_path: str = "data/templating/collections/terraform-init"
39
48
 
40
49
 
@@ -55,25 +64,35 @@ class TerraformInitIntegration(
55
64
  query_func = gql.get_api().query
56
65
  return {
57
66
  "accounts": [
58
- account.dict() for account in self.get_aws_accounts(query_func)
67
+ account.model_dump() for account in self.get_aws_accounts(query_func)
59
68
  ],
60
69
  }
61
70
 
62
71
  def get_aws_accounts(
63
72
  self, query_func: Callable, account_name: str | None = None
64
73
  ) -> list[AWSAccountV1]:
65
- """Return all AWS accounts with terraform username but no terraform state set."""
74
+ """Return all AWS accounts with terraform username."""
66
75
  return [
67
76
  account
68
77
  for account in aws_accounts_query(query_func).accounts or []
69
78
  if integration_is_enabled(self.name, account)
70
79
  and (not account_name or account.name == account_name)
71
80
  and account.terraform_username
72
- and not account.terraform_state
73
81
  ]
74
82
 
83
+ @staticmethod
84
+ def get_default_tags(gql_api: GqlApi) -> dict[str, str]:
85
+ try:
86
+ return get_settings(gql_api.query).default_tags
87
+ except ValueError:
88
+ # no settings found
89
+ return {}
90
+
91
+ @staticmethod
75
92
  def render_state_collection(
76
- self, template: str, bucket_name: str, account: AWSAccountV1
93
+ template: str,
94
+ bucket_name: str,
95
+ account: AWSAccountV1,
77
96
  ) -> str:
78
97
  return jinja2.Template(
79
98
  template,
@@ -85,24 +104,114 @@ class TerraformInitIntegration(
85
104
  "account_name": account.name,
86
105
  "bucket_name": bucket_name,
87
106
  "region": account.resources_default_region,
88
- "timestamp": int(datetime.now(tz=UTC).timestamp()),
107
+ "timestamp": int(utc_now().timestamp()),
89
108
  })
90
109
 
91
110
  def reconcile_account(
92
111
  self,
93
- account_aws_api: AWSApi,
112
+ aws_api: AWSApi,
113
+ merge_request_manager: MergeRequestManager,
114
+ dry_run: bool,
115
+ account: AWSAccountV1,
116
+ state_template: str,
117
+ cloudformation_template: str,
118
+ cloudformation_import_template: str,
119
+ default_tags: dict[str, str],
120
+ ) -> None:
121
+ """
122
+ Reconcile the terraform state for a given account.
123
+
124
+ Create S3 bucket via CloudFormation and Merge Request to update template file on init.
125
+ Import existing bucket if it exists but not managed by CloudFormation.
126
+ Update CloudFormation stack if tags or template body differ.
127
+
128
+ CloudFormation stack name and bucket name is `terraform-<account_name>`.
129
+ `cloudformation_import_template` should be minimal template with identifier fields only.
130
+ `cloudformation_template` should be the full template.
131
+ Use import template to import then update stack to match full template.
132
+ This will ensure imported resources match CloudFormation template.
133
+ And all desired changes in full template are applied.
134
+ Both templates must have BucketName in Parameters.
135
+
136
+ Args:
137
+ aws_api: AWSApi: AWS API client for the target account.
138
+ merge_request_manager: MergeRequestManager: Manager to handle merge requests.
139
+ dry_run: bool: If True, do not make any changes.
140
+ account: AWSAccountV1: The AWS account to reconcile.
141
+ state_template: str: Jinja2 template for the Terraform state configuration.
142
+ cloudformation_template: str: CloudFormation template to create the S3 bucket.
143
+ cloudformation_import_template: str: CloudFormation template to import existing S3 bucket.
144
+ default_tags: dict[str, str]: Default tags to apply to the CloudFormation stack.
145
+
146
+ Returns:
147
+ None
148
+ """
149
+ bucket_name = (
150
+ account.terraform_state.bucket
151
+ if account.terraform_state
152
+ else f"terraform-{account.name}"
153
+ )
154
+
155
+ tags = default_tags | get_aws_account_tags(account.organization)
156
+
157
+ if account.terraform_state is None:
158
+ return self._provision_terraform_state(
159
+ aws_api=aws_api,
160
+ merge_request_manager=merge_request_manager,
161
+ dry_run=dry_run,
162
+ account=account,
163
+ bucket_name=bucket_name,
164
+ cloudformation_template=cloudformation_template,
165
+ state_template=state_template,
166
+ tags=tags,
167
+ )
168
+
169
+ stack = aws_api.cloudformation.get_stack(stack_name=bucket_name)
170
+
171
+ if stack is None:
172
+ return self._import_cloudformation_stack(
173
+ aws_api=aws_api,
174
+ dry_run=dry_run,
175
+ bucket_name=bucket_name,
176
+ cloudformation_import_template=cloudformation_import_template,
177
+ cloudformation_template=cloudformation_template,
178
+ tags=tags,
179
+ )
180
+
181
+ return self._reconcile_cloudformation_stack(
182
+ aws_api=aws_api,
183
+ dry_run=dry_run,
184
+ bucket_name=bucket_name,
185
+ cloudformation_template=cloudformation_template,
186
+ tags=tags,
187
+ current_tags={tag["Key"]: tag["Value"] for tag in stack.get("Tags", [])},
188
+ )
189
+
190
+ def _provision_terraform_state(
191
+ self,
192
+ aws_api: AWSApi,
94
193
  merge_request_manager: MergeRequestManager,
95
194
  dry_run: bool,
96
- state_collection: str,
97
- bucket_name: str,
98
195
  account: AWSAccountV1,
196
+ bucket_name: str,
197
+ cloudformation_template: str,
198
+ state_template: str,
199
+ tags: dict[str, str],
99
200
  ) -> None:
100
201
  logging.info("Creating bucket '%s' for account '%s'", bucket_name, account.name)
101
202
  if not dry_run:
102
- # the creation of the bucket is idempotent
103
- account_aws_api.s3.create_bucket(
104
- name=bucket_name, region=account.resources_default_region
203
+ aws_api.cloudformation.create_stack(
204
+ stack_name=bucket_name,
205
+ change_set_name=f"create-{bucket_name}",
206
+ template_body=cloudformation_template,
207
+ parameters={"BucketName": bucket_name},
208
+ tags=tags,
105
209
  )
210
+ state_collection = self.render_state_collection(
211
+ template=state_template,
212
+ bucket_name=bucket_name,
213
+ account=account,
214
+ )
106
215
  merge_request_manager.create_merge_request(
107
216
  data=MrData(
108
217
  account=account.name,
@@ -111,6 +220,55 @@ class TerraformInitIntegration(
111
220
  )
112
221
  )
113
222
 
223
+ @staticmethod
224
+ def _import_cloudformation_stack(
225
+ aws_api: AWSApi,
226
+ dry_run: bool,
227
+ bucket_name: str,
228
+ cloudformation_import_template: str,
229
+ cloudformation_template: str,
230
+ tags: dict[str, str],
231
+ ) -> None:
232
+ logging.info("Importing existing bucket %s", bucket_name)
233
+ if not dry_run:
234
+ aws_api.cloudformation.create_stack(
235
+ stack_name=bucket_name,
236
+ change_set_name=f"import-{bucket_name}",
237
+ template_body=cloudformation_import_template,
238
+ parameters={"BucketName": bucket_name},
239
+ tags=tags,
240
+ )
241
+ logging.info("Syncing stack %s after import", bucket_name)
242
+ aws_api.cloudformation.update_stack(
243
+ stack_name=bucket_name,
244
+ template_body=cloudformation_template,
245
+ parameters={"BucketName": bucket_name},
246
+ tags=tags,
247
+ )
248
+
249
+ @staticmethod
250
+ def _reconcile_cloudformation_stack(
251
+ aws_api: AWSApi,
252
+ dry_run: bool,
253
+ bucket_name: str,
254
+ cloudformation_template: str,
255
+ tags: dict[str, str],
256
+ current_tags: dict[str, str],
257
+ ) -> None:
258
+ if (
259
+ current_tags != tags
260
+ or aws_api.cloudformation.get_template_body(stack_name=bucket_name)
261
+ != cloudformation_template
262
+ ):
263
+ logging.info("Updating stack %s", bucket_name)
264
+ if not dry_run:
265
+ aws_api.cloudformation.update_stack(
266
+ stack_name=bucket_name,
267
+ template_body=cloudformation_template,
268
+ parameters={"BucketName": bucket_name},
269
+ tags=tags,
270
+ )
271
+
114
272
  @defer
115
273
  def run(self, dry_run: bool, defer: Callable | None = None) -> None:
116
274
  """Run the integration."""
@@ -144,6 +302,14 @@ class TerraformInitIntegration(
144
302
  state_template = gql_api.get_resource(path=self.params.state_tmpl_resource)[
145
303
  "content"
146
304
  ]
305
+ cloudformation_template = gql_api.get_resource(
306
+ path=self.params.cloudformation_template_resource
307
+ )["content"]
308
+ cloudformation_import_template = gql_api.get_resource(
309
+ path=self.params.cloudformation_import_template_resource
310
+ )["content"]
311
+ default_tags = self.get_default_tags(gql_api)
312
+
147
313
  for account in accounts:
148
314
  secret = self.secret_reader.read_all_secret(account.automation_token)
149
315
  with AWSApi(
@@ -153,15 +319,13 @@ class TerraformInitIntegration(
153
319
  region=account.resources_default_region,
154
320
  )
155
321
  ) as account_aws_api:
156
- bucket_name = f"terraform-{account.name}"
157
- state_collection = self.render_state_collection(
158
- state_template, bucket_name, account
159
- )
160
322
  self.reconcile_account(
161
- account_aws_api,
162
- merge_request_manager,
163
- dry_run,
164
- state_collection,
165
- bucket_name,
166
- account,
323
+ aws_api=account_aws_api,
324
+ merge_request_manager=merge_request_manager,
325
+ dry_run=dry_run,
326
+ account=account,
327
+ state_template=state_template,
328
+ cloudformation_template=cloudformation_template,
329
+ cloudformation_import_template=cloudformation_import_template,
330
+ default_tags=default_tags,
167
331
  )