qontract-reconcile 0.10.2.dev361__py3-none-any.whl → 0.10.2.dev474__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. {qontract_reconcile-0.10.2.dev361.dist-info → qontract_reconcile-0.10.2.dev474.dist-info}/METADATA +14 -13
  2. {qontract_reconcile-0.10.2.dev361.dist-info → qontract_reconcile-0.10.2.dev474.dist-info}/RECORD +371 -364
  3. {qontract_reconcile-0.10.2.dev361.dist-info → qontract_reconcile-0.10.2.dev474.dist-info}/WHEEL +1 -1
  4. reconcile/acs_rbac.py +2 -2
  5. reconcile/aus/advanced_upgrade_service.py +18 -12
  6. reconcile/aus/aus_sts_gate_handler.py +59 -0
  7. reconcile/aus/base.py +137 -34
  8. reconcile/aus/cluster_version_data.py +15 -5
  9. reconcile/aus/models.py +3 -1
  10. reconcile/aus/ocm_addons_upgrade_scheduler_org.py +1 -0
  11. reconcile/aus/ocm_upgrade_scheduler.py +8 -1
  12. reconcile/aus/ocm_upgrade_scheduler_org.py +20 -5
  13. reconcile/aus/version_gate_approver.py +1 -16
  14. reconcile/aus/version_gates/sts_version_gate_handler.py +5 -72
  15. reconcile/automated_actions/config/integration.py +16 -4
  16. reconcile/aws_account_manager/integration.py +21 -9
  17. reconcile/aws_account_manager/reconciler.py +3 -3
  18. reconcile/aws_account_manager/utils.py +1 -1
  19. reconcile/aws_ami_cleanup/integration.py +8 -12
  20. reconcile/aws_ami_share.py +69 -62
  21. reconcile/aws_cloudwatch_log_retention/integration.py +155 -126
  22. reconcile/aws_ecr_image_pull_secrets.py +1 -1
  23. reconcile/aws_iam_keys.py +1 -0
  24. reconcile/aws_saml_idp/integration.py +12 -4
  25. reconcile/aws_saml_roles/integration.py +30 -23
  26. reconcile/aws_version_sync/integration.py +6 -12
  27. reconcile/change_owners/README.md +1 -1
  28. reconcile/change_owners/bundle.py +3 -3
  29. reconcile/change_owners/change_log_tracking.py +3 -2
  30. reconcile/change_owners/change_owners.py +108 -42
  31. reconcile/change_owners/decision.py +1 -1
  32. reconcile/change_owners/diff.py +0 -2
  33. reconcile/checkpoint.py +11 -3
  34. reconcile/cli.py +94 -11
  35. reconcile/dashdotdb_dora.py +5 -12
  36. reconcile/dashdotdb_slo.py +1 -1
  37. reconcile/database_access_manager.py +123 -117
  38. reconcile/dynatrace_token_provider/integration.py +1 -1
  39. reconcile/endpoints_discovery/integration.py +4 -1
  40. reconcile/endpoints_discovery/merge_request.py +1 -1
  41. reconcile/endpoints_discovery/merge_request_manager.py +8 -8
  42. reconcile/external_resources/factories.py +4 -6
  43. reconcile/external_resources/integration.py +1 -1
  44. reconcile/external_resources/manager.py +8 -6
  45. reconcile/external_resources/meta.py +0 -1
  46. reconcile/external_resources/metrics.py +1 -1
  47. reconcile/external_resources/model.py +19 -15
  48. reconcile/external_resources/reconciler.py +7 -4
  49. reconcile/external_resources/secrets_sync.py +6 -10
  50. reconcile/external_resources/state.py +26 -16
  51. reconcile/fleet_labeler/integration.py +1 -1
  52. reconcile/gabi_authorized_users.py +5 -2
  53. reconcile/gcp_image_mirror.py +2 -2
  54. reconcile/github_org.py +1 -1
  55. reconcile/github_owners.py +4 -0
  56. reconcile/gitlab_housekeeping.py +13 -15
  57. reconcile/gitlab_members.py +6 -12
  58. reconcile/gitlab_owners.py +15 -11
  59. reconcile/gitlab_permissions.py +8 -12
  60. reconcile/glitchtip_project_alerts/integration.py +3 -1
  61. reconcile/gql_definitions/acs/acs_instances.py +5 -5
  62. reconcile/gql_definitions/acs/acs_policies.py +5 -5
  63. reconcile/gql_definitions/acs/acs_rbac.py +5 -5
  64. reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py +5 -5
  65. reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py +5 -5
  66. reconcile/gql_definitions/app_interface_metrics_exporter/onboarding_status.py +5 -5
  67. reconcile/gql_definitions/app_sre_tekton_access_revalidation/roles.py +5 -5
  68. reconcile/gql_definitions/app_sre_tekton_access_revalidation/users.py +5 -5
  69. reconcile/gql_definitions/automated_actions/instance.py +46 -7
  70. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +14 -5
  71. reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +15 -5
  72. reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +27 -66
  73. reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +15 -5
  74. reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +15 -5
  75. reconcile/gql_definitions/aws_saml_roles/roles.py +5 -5
  76. reconcile/gql_definitions/aws_version_sync/clusters.py +5 -5
  77. reconcile/gql_definitions/aws_version_sync/namespaces.py +5 -5
  78. reconcile/gql_definitions/change_owners/queries/change_types.py +5 -5
  79. reconcile/gql_definitions/change_owners/queries/self_service_roles.py +5 -5
  80. reconcile/gql_definitions/cluster_auth_rhidp/clusters.py +5 -5
  81. reconcile/gql_definitions/common/alerting_services_settings.py +5 -5
  82. reconcile/gql_definitions/common/app_code_component_repos.py +5 -5
  83. reconcile/gql_definitions/common/app_interface_custom_messages.py +5 -5
  84. reconcile/gql_definitions/common/app_interface_dms_settings.py +5 -5
  85. reconcile/gql_definitions/common/app_interface_repo_settings.py +5 -5
  86. reconcile/gql_definitions/common/app_interface_roles.py +5 -5
  87. reconcile/gql_definitions/common/app_interface_state_settings.py +5 -5
  88. reconcile/gql_definitions/common/app_interface_vault_settings.py +5 -5
  89. reconcile/gql_definitions/common/app_quay_repos_escalation_policies.py +5 -5
  90. reconcile/gql_definitions/common/apps.py +5 -5
  91. reconcile/gql_definitions/common/aws_vpc_requests.py +18 -5
  92. reconcile/gql_definitions/common/aws_vpcs.py +5 -5
  93. reconcile/gql_definitions/common/clusters.py +7 -5
  94. reconcile/gql_definitions/common/clusters_minimal.py +5 -5
  95. reconcile/gql_definitions/common/clusters_with_dms.py +5 -5
  96. reconcile/gql_definitions/common/clusters_with_peering.py +5 -5
  97. reconcile/gql_definitions/common/github_orgs.py +5 -5
  98. reconcile/gql_definitions/common/jira_settings.py +5 -5
  99. reconcile/gql_definitions/common/jiralert_settings.py +5 -5
  100. reconcile/gql_definitions/common/ldap_settings.py +5 -5
  101. reconcile/gql_definitions/common/namespaces.py +5 -5
  102. reconcile/gql_definitions/common/namespaces_minimal.py +7 -5
  103. reconcile/gql_definitions/common/ocm_env_telemeter.py +5 -5
  104. reconcile/gql_definitions/common/ocm_environments.py +5 -5
  105. reconcile/gql_definitions/common/pagerduty_instances.py +5 -5
  106. reconcile/gql_definitions/common/pgp_reencryption_settings.py +5 -5
  107. reconcile/gql_definitions/common/pipeline_providers.py +5 -5
  108. reconcile/gql_definitions/common/quay_instances.py +5 -5
  109. reconcile/gql_definitions/common/quay_orgs.py +5 -5
  110. reconcile/gql_definitions/common/reserved_networks.py +5 -5
  111. reconcile/gql_definitions/common/rhcs_provider_settings.py +5 -5
  112. reconcile/gql_definitions/common/saas_files.py +5 -5
  113. reconcile/gql_definitions/common/saas_target_namespaces.py +5 -5
  114. reconcile/gql_definitions/common/saasherder_settings.py +5 -5
  115. reconcile/gql_definitions/common/slack_workspaces.py +5 -5
  116. reconcile/gql_definitions/common/smtp_client_settings.py +5 -5
  117. reconcile/gql_definitions/common/state_aws_account.py +5 -5
  118. reconcile/gql_definitions/common/users.py +5 -5
  119. reconcile/gql_definitions/common/users_with_paths.py +5 -5
  120. reconcile/gql_definitions/cost_report/app_names.py +5 -5
  121. reconcile/gql_definitions/cost_report/cost_namespaces.py +5 -5
  122. reconcile/gql_definitions/cost_report/settings.py +5 -5
  123. reconcile/gql_definitions/dashdotdb_slo/slo_documents_query.py +5 -5
  124. reconcile/gql_definitions/dynatrace_token_provider/dynatrace_bootstrap_tokens.py +5 -5
  125. reconcile/gql_definitions/dynatrace_token_provider/token_specs.py +5 -5
  126. reconcile/gql_definitions/email_sender/apps.py +5 -5
  127. reconcile/gql_definitions/email_sender/emails.py +5 -5
  128. reconcile/gql_definitions/email_sender/users.py +5 -5
  129. reconcile/gql_definitions/endpoints_discovery/apps.py +5 -5
  130. reconcile/gql_definitions/external_resources/aws_accounts.py +5 -5
  131. reconcile/gql_definitions/external_resources/external_resources_modules.py +5 -5
  132. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +38 -7
  133. reconcile/gql_definitions/external_resources/external_resources_settings.py +5 -5
  134. reconcile/gql_definitions/external_resources/fragments/external_resources_module_overrides.py +5 -5
  135. reconcile/gql_definitions/fleet_labeler/fleet_labels.py +5 -5
  136. reconcile/gql_definitions/fragments/aus_organization.py +5 -5
  137. reconcile/gql_definitions/fragments/aws_account_common.py +7 -5
  138. reconcile/gql_definitions/fragments/aws_account_managed.py +5 -5
  139. reconcile/gql_definitions/fragments/aws_account_sso.py +5 -5
  140. reconcile/gql_definitions/fragments/aws_infra_management_account.py +5 -5
  141. reconcile/gql_definitions/fragments/aws_organization.py +33 -0
  142. reconcile/gql_definitions/fragments/aws_vpc.py +5 -5
  143. reconcile/gql_definitions/fragments/aws_vpc_request.py +12 -5
  144. reconcile/gql_definitions/fragments/container_image_mirror.py +5 -5
  145. reconcile/gql_definitions/fragments/deploy_resources.py +5 -5
  146. reconcile/gql_definitions/fragments/disable.py +5 -5
  147. reconcile/gql_definitions/fragments/email_service.py +5 -5
  148. reconcile/gql_definitions/fragments/email_user.py +5 -5
  149. reconcile/gql_definitions/fragments/jumphost_common_fields.py +5 -5
  150. reconcile/gql_definitions/fragments/membership_source.py +5 -5
  151. reconcile/gql_definitions/fragments/minimal_ocm_organization.py +5 -5
  152. reconcile/gql_definitions/fragments/oc_connection_cluster.py +5 -5
  153. reconcile/gql_definitions/fragments/ocm_environment.py +5 -5
  154. reconcile/gql_definitions/fragments/pipeline_provider_retention.py +5 -5
  155. reconcile/gql_definitions/fragments/prometheus_instance.py +5 -5
  156. reconcile/gql_definitions/fragments/resource_limits_requirements.py +5 -5
  157. reconcile/gql_definitions/fragments/resource_requests_requirements.py +5 -5
  158. reconcile/gql_definitions/fragments/resource_values.py +5 -5
  159. reconcile/gql_definitions/fragments/saas_slo_document.py +5 -5
  160. reconcile/gql_definitions/fragments/saas_target_namespace.py +5 -5
  161. reconcile/gql_definitions/fragments/serviceaccount_token.py +5 -5
  162. reconcile/gql_definitions/fragments/terraform_state.py +5 -5
  163. reconcile/gql_definitions/fragments/upgrade_policy.py +5 -5
  164. reconcile/gql_definitions/fragments/user.py +5 -5
  165. reconcile/gql_definitions/fragments/vault_secret.py +5 -5
  166. reconcile/gql_definitions/gcp/gcp_docker_repos.py +5 -5
  167. reconcile/gql_definitions/gcp/gcp_projects.py +5 -5
  168. reconcile/gql_definitions/gitlab_members/gitlab_instances.py +5 -5
  169. reconcile/gql_definitions/gitlab_members/permissions.py +5 -5
  170. reconcile/gql_definitions/glitchtip/glitchtip_instance.py +5 -5
  171. reconcile/gql_definitions/glitchtip/glitchtip_project.py +5 -5
  172. reconcile/gql_definitions/glitchtip_project_alerts/glitchtip_project.py +5 -5
  173. reconcile/gql_definitions/integrations/integrations.py +5 -5
  174. reconcile/gql_definitions/introspection.json +775 -136
  175. reconcile/gql_definitions/jenkins_configs/jenkins_configs.py +5 -5
  176. reconcile/gql_definitions/jenkins_configs/jenkins_instances.py +5 -5
  177. reconcile/gql_definitions/jira/jira_servers.py +5 -5
  178. reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +9 -5
  179. reconcile/gql_definitions/jumphosts/jumphosts.py +5 -5
  180. reconcile/gql_definitions/ldap_groups/roles.py +5 -5
  181. reconcile/gql_definitions/ldap_groups/settings.py +5 -5
  182. reconcile/gql_definitions/maintenance/maintenances.py +5 -5
  183. reconcile/gql_definitions/membershipsources/roles.py +5 -5
  184. reconcile/gql_definitions/ocm_labels/clusters.py +5 -5
  185. reconcile/gql_definitions/ocm_labels/organizations.py +5 -5
  186. reconcile/gql_definitions/openshift_cluster_bots/clusters.py +5 -5
  187. reconcile/gql_definitions/openshift_groups/managed_groups.py +5 -5
  188. reconcile/gql_definitions/openshift_groups/managed_roles.py +5 -5
  189. reconcile/gql_definitions/openshift_serviceaccount_tokens/tokens.py +5 -5
  190. reconcile/gql_definitions/quay_membership/quay_membership.py +5 -5
  191. reconcile/gql_definitions/rhcs/certs.py +25 -79
  192. reconcile/gql_definitions/rhcs/openshift_resource_rhcs_cert.py +43 -0
  193. reconcile/gql_definitions/rhidp/organizations.py +5 -5
  194. reconcile/gql_definitions/service_dependencies/jenkins_instance_fragment.py +5 -5
  195. reconcile/gql_definitions/service_dependencies/service_dependencies.py +5 -5
  196. reconcile/gql_definitions/sharding/aws_accounts.py +5 -5
  197. reconcile/gql_definitions/sharding/ocm_organization.py +5 -5
  198. reconcile/gql_definitions/skupper_network/site_controller_template.py +5 -5
  199. reconcile/gql_definitions/skupper_network/skupper_networks.py +5 -5
  200. reconcile/gql_definitions/slack_usergroups/clusters.py +5 -5
  201. reconcile/gql_definitions/slack_usergroups/permissions.py +5 -5
  202. reconcile/gql_definitions/slack_usergroups/users.py +5 -5
  203. reconcile/gql_definitions/slo_documents/slo_documents.py +5 -5
  204. reconcile/gql_definitions/status_board/status_board.py +5 -5
  205. reconcile/gql_definitions/statuspage/statuspages.py +5 -5
  206. reconcile/gql_definitions/templating/template_collection.py +5 -5
  207. reconcile/gql_definitions/templating/templates.py +5 -5
  208. reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py +5 -5
  209. reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py +5 -5
  210. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_accounts.py +5 -5
  211. reconcile/gql_definitions/terraform_cloudflare_resources/terraform_cloudflare_resources.py +5 -5
  212. reconcile/gql_definitions/terraform_cloudflare_users/app_interface_setting_cloudflare_and_vault.py +5 -5
  213. reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.py +5 -5
  214. reconcile/gql_definitions/terraform_init/aws_accounts.py +19 -5
  215. reconcile/gql_definitions/terraform_repo/terraform_repo.py +5 -5
  216. reconcile/gql_definitions/terraform_resources/database_access_manager.py +5 -5
  217. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +37 -7
  218. reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +15 -5
  219. reconcile/gql_definitions/unleash_feature_toggles/feature_toggles.py +5 -5
  220. reconcile/gql_definitions/vault_instances/vault_instances.py +5 -5
  221. reconcile/gql_definitions/vault_policies/vault_policies.py +5 -5
  222. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator.py +8 -5
  223. reconcile/gql_definitions/vpc_peerings_validator/vpc_peerings_validator_peered_cluster_fragment.py +6 -5
  224. reconcile/integrations_manager.py +3 -3
  225. reconcile/jenkins_worker_fleets.py +10 -8
  226. reconcile/jira_permissions_validator.py +237 -122
  227. reconcile/ldap_groups/integration.py +1 -1
  228. reconcile/ocm/types.py +35 -57
  229. reconcile/ocm_aws_infrastructure_access.py +1 -1
  230. reconcile/ocm_clusters.py +4 -4
  231. reconcile/ocm_labels/integration.py +3 -2
  232. reconcile/ocm_machine_pools.py +33 -27
  233. reconcile/openshift_base.py +113 -4
  234. reconcile/openshift_cluster_bots.py +1 -1
  235. reconcile/openshift_namespace_labels.py +1 -1
  236. reconcile/openshift_namespaces.py +96 -101
  237. reconcile/openshift_resources_base.py +6 -2
  238. reconcile/openshift_rhcs_certs.py +74 -37
  239. reconcile/openshift_rolebindings.py +7 -11
  240. reconcile/openshift_saas_deploy.py +4 -5
  241. reconcile/openshift_saas_deploy_change_tester.py +9 -7
  242. reconcile/openshift_saas_deploy_trigger_cleaner.py +3 -5
  243. reconcile/openshift_serviceaccount_tokens.py +2 -2
  244. reconcile/openshift_upgrade_watcher.py +4 -4
  245. reconcile/oum/labelset.py +5 -3
  246. reconcile/oum/models.py +1 -4
  247. reconcile/prometheus_rules_tester/integration.py +3 -3
  248. reconcile/quay_base.py +25 -6
  249. reconcile/quay_membership.py +55 -29
  250. reconcile/quay_mirror.py +1 -1
  251. reconcile/quay_mirror_org.py +6 -4
  252. reconcile/quay_permissions.py +81 -75
  253. reconcile/quay_repos.py +35 -37
  254. reconcile/queries.py +132 -1
  255. reconcile/rhidp/common.py +3 -5
  256. reconcile/rhidp/sso_client/base.py +16 -5
  257. reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py +1 -1
  258. reconcile/saas_auto_promotions_manager/subscriber.py +4 -3
  259. reconcile/skupper_network/integration.py +2 -2
  260. reconcile/slack_usergroups.py +35 -14
  261. reconcile/sql_query.py +1 -0
  262. reconcile/status_board.py +6 -6
  263. reconcile/statuspage/atlassian.py +7 -7
  264. reconcile/statuspage/integrations/maintenances.py +4 -3
  265. reconcile/statuspage/page.py +4 -9
  266. reconcile/statuspage/status.py +5 -8
  267. reconcile/templates/rosa-classic-cluster-creation.sh.j2 +1 -1
  268. reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +1 -1
  269. reconcile/templating/lib/rendering.py +3 -3
  270. reconcile/templating/renderer.py +2 -2
  271. reconcile/templating/validator.py +4 -4
  272. reconcile/terraform_aws_route53.py +7 -1
  273. reconcile/terraform_cloudflare_dns.py +3 -3
  274. reconcile/terraform_cloudflare_resources.py +5 -5
  275. reconcile/terraform_cloudflare_users.py +3 -2
  276. reconcile/terraform_init/integration.py +187 -23
  277. reconcile/terraform_repo.py +16 -12
  278. reconcile/terraform_resources.py +6 -6
  279. reconcile/terraform_tgw_attachments.py +27 -19
  280. reconcile/terraform_users.py +7 -0
  281. reconcile/terraform_vpc_peerings.py +14 -3
  282. reconcile/terraform_vpc_resources/integration.py +20 -8
  283. reconcile/terraform_vpc_resources/merge_request.py +12 -2
  284. reconcile/terraform_vpc_resources/merge_request_manager.py +43 -19
  285. reconcile/typed_queries/aws_account_tags.py +41 -0
  286. reconcile/typed_queries/cost_report/app_names.py +1 -1
  287. reconcile/typed_queries/cost_report/cost_namespaces.py +2 -2
  288. reconcile/typed_queries/saas_files.py +20 -15
  289. reconcile/typed_queries/status_board.py +2 -2
  290. reconcile/unleash_feature_toggles/integration.py +4 -2
  291. reconcile/utils/acs/base.py +6 -3
  292. reconcile/utils/acs/policies.py +2 -2
  293. reconcile/utils/aws_api.py +51 -20
  294. reconcile/utils/aws_api_typed/api.py +38 -9
  295. reconcile/utils/aws_api_typed/cloudformation.py +149 -0
  296. reconcile/utils/aws_api_typed/logs.py +73 -0
  297. reconcile/utils/aws_api_typed/organization.py +4 -2
  298. reconcile/utils/binary.py +7 -12
  299. reconcile/utils/datetime_util.py +67 -0
  300. reconcile/utils/deadmanssnitch_api.py +1 -1
  301. reconcile/utils/differ.py +2 -3
  302. reconcile/utils/early_exit_cache.py +11 -12
  303. reconcile/utils/environ.py +5 -0
  304. reconcile/utils/expiration.py +7 -3
  305. reconcile/utils/external_resource_spec.py +2 -0
  306. reconcile/utils/filtering.py +1 -1
  307. reconcile/utils/gitlab_api.py +19 -5
  308. reconcile/utils/glitchtip/client.py +6 -2
  309. reconcile/utils/glitchtip/models.py +25 -28
  310. reconcile/utils/gql.py +4 -7
  311. reconcile/utils/helpers.py +1 -1
  312. reconcile/utils/instrumented_wrappers.py +1 -1
  313. reconcile/utils/internal_groups/client.py +2 -2
  314. reconcile/utils/internal_groups/models.py +8 -17
  315. reconcile/utils/jinja2/utils.py +6 -101
  316. reconcile/utils/jira_client.py +82 -63
  317. reconcile/utils/jjb_client.py +26 -13
  318. reconcile/utils/jobcontroller/controller.py +2 -2
  319. reconcile/utils/jobcontroller/models.py +17 -1
  320. reconcile/utils/json.py +43 -1
  321. reconcile/utils/membershipsources/app_interface_resolver.py +4 -2
  322. reconcile/utils/membershipsources/models.py +16 -23
  323. reconcile/utils/membershipsources/resolver.py +4 -2
  324. reconcile/utils/merge_request_manager/merge_request_manager.py +4 -4
  325. reconcile/utils/merge_request_manager/parser.py +6 -6
  326. reconcile/utils/metrics.py +5 -5
  327. reconcile/utils/models.py +304 -82
  328. reconcile/utils/mr/app_interface_reporter.py +2 -2
  329. reconcile/utils/mr/notificator.py +3 -3
  330. reconcile/utils/mr/update_access_report_base.py +3 -4
  331. reconcile/utils/mr/user_maintenance.py +3 -2
  332. reconcile/utils/oc.py +252 -201
  333. reconcile/utils/oc_filters.py +3 -3
  334. reconcile/utils/ocm/addons.py +0 -1
  335. reconcile/utils/ocm/base.py +17 -20
  336. reconcile/utils/ocm/cluster_groups.py +1 -1
  337. reconcile/utils/ocm/identity_providers.py +2 -2
  338. reconcile/utils/ocm/labels.py +1 -1
  339. reconcile/utils/ocm/products.py +8 -8
  340. reconcile/utils/ocm/search_filters.py +3 -6
  341. reconcile/utils/ocm/service_log.py +4 -6
  342. reconcile/utils/ocm/sre_capability_labels.py +20 -13
  343. reconcile/utils/openshift_resource.py +8 -3
  344. reconcile/utils/pagerduty_api.py +10 -7
  345. reconcile/utils/promotion_state.py +6 -11
  346. reconcile/utils/quay_api.py +74 -87
  347. reconcile/utils/raw_github_api.py +1 -1
  348. reconcile/utils/rhcsv2_certs.py +86 -23
  349. reconcile/utils/rosa/session.py +16 -0
  350. reconcile/utils/runtime/integration.py +2 -3
  351. reconcile/utils/runtime/runner.py +2 -2
  352. reconcile/utils/saasherder/interfaces.py +13 -20
  353. reconcile/utils/saasherder/models.py +23 -20
  354. reconcile/utils/saasherder/saasherder.py +50 -27
  355. reconcile/utils/slack_api.py +2 -2
  356. reconcile/utils/sloth.py +171 -2
  357. reconcile/utils/structs.py +1 -1
  358. reconcile/utils/terraform_client.py +5 -4
  359. reconcile/utils/terrascript_aws_client.py +274 -124
  360. reconcile/utils/unleash/server.py +2 -8
  361. reconcile/utils/vault.py +5 -12
  362. reconcile/utils/vcs.py +8 -8
  363. reconcile/vault_replication.py +107 -42
  364. reconcile/vpc_peerings_validator.py +13 -0
  365. tools/app_interface_reporter.py +4 -4
  366. tools/cli_commands/cost_report/cost_management_api.py +3 -3
  367. tools/cli_commands/cost_report/view.py +7 -6
  368. tools/cli_commands/erv2.py +1 -1
  369. tools/qontract_cli.py +28 -17
  370. tools/template_validation.py +3 -1
  371. {qontract_reconcile-0.10.2.dev361.dist-info → qontract_reconcile-0.10.2.dev474.dist-info}/entry_points.txt +0 -0
@@ -22,7 +22,6 @@ from typing import (
22
22
  TYPE_CHECKING,
23
23
  Any,
24
24
  Self,
25
- TypeAlias,
26
25
  cast,
27
26
  )
28
27
 
@@ -152,6 +151,7 @@ from reconcile.github_org import get_default_config
152
151
  from reconcile.gql_definitions.terraform_resources.terraform_resources_namespaces import (
153
152
  NamespaceTerraformResourceLifecycleV1,
154
153
  )
154
+ from reconcile.typed_queries.aws_account_tags import get_aws_account_tags
155
155
  from reconcile.utils import gql
156
156
  from reconcile.utils.aws_api import (
157
157
  AmiTag,
@@ -178,7 +178,10 @@ from reconcile.utils.external_resources import (
178
178
  from reconcile.utils.git import is_file_in_git_repo
179
179
  from reconcile.utils.gitlab_api import GitLabApi
180
180
  from reconcile.utils.jenkins_api import JenkinsApi
181
- from reconcile.utils.jinja2.utils import process_extracurlyjinja2_template
181
+ from reconcile.utils.jinja2.utils import (
182
+ process_extracurlyjinja2_template,
183
+ process_jinja2_template,
184
+ )
182
185
  from reconcile.utils.json import json_dumps
183
186
  from reconcile.utils.password_validator import (
184
187
  PasswordPolicy,
@@ -203,7 +206,7 @@ if TYPE_CHECKING:
203
206
  from reconcile.utils.ocm import OCMMap
204
207
 
205
208
 
206
- TFResource: TypeAlias = type[
209
+ type TFResource = type[
207
210
  Resource | Data | Module | Provider | Variable | Output | Locals | Terraform
208
211
  ]
209
212
 
@@ -268,6 +271,8 @@ VARIABLE_KEYS = [
268
271
  "extra_tags",
269
272
  "lifecycle",
270
273
  "max_session_duration",
274
+ "secret_format",
275
+ "policy",
271
276
  ]
272
277
 
273
278
  EMAIL_REGEX = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
@@ -369,6 +374,10 @@ class aws_s3_bucket_logging(Resource):
369
374
  pass
370
375
 
371
376
 
377
+ class aws_kinesis_resource_policy(Resource):
378
+ pass
379
+
380
+
372
381
  class aws_cloudfront_log_delivery_canonical_user_id(Data):
373
382
  pass
374
383
 
@@ -470,10 +479,10 @@ class TerrascriptClient:
470
479
  integration_prefix: str,
471
480
  thread_pool_size: int,
472
481
  accounts: Iterable[MutableMapping[str, Any]],
482
+ default_tags: Mapping[str, str] | None,
473
483
  settings: Mapping[str, Any] | None = None,
474
484
  prefetch_resources_by_schemas: Iterable[str] | None = None,
475
485
  secret_reader: SecretReaderBase | None = None,
476
- default_tags: Mapping[str, str] | None = None,
477
486
  ) -> None:
478
487
  self.integration = integration
479
488
  self.integration_prefix = integration_prefix
@@ -484,16 +493,11 @@ class TerrascriptClient:
484
493
  else:
485
494
  self.secret_reader = SecretReader(settings=settings)
486
495
  self.configs: dict[str, dict] = {}
496
+ self.default_tags = default_tags or {"app": "app-sre-infra"}
487
497
  self.populate_configs(filtered_accounts)
488
498
  self.versions: dict[str, str] = {
489
499
  a["name"]: a["providerVersion"] for a in filtered_accounts
490
500
  }
491
- self.default_tags = {
492
- "tags": default_tags
493
- or {
494
- "app": "app-sre-infra",
495
- }
496
- }
497
501
  tss = {}
498
502
  locks = {}
499
503
  self.supported_regions = {}
@@ -510,7 +514,7 @@ class TerrascriptClient:
510
514
  region=region,
511
515
  alias=region,
512
516
  skip_region_validation=True,
513
- default_tags=self.default_tags,
517
+ default_tags={"tags": config["tags"]},
514
518
  )
515
519
 
516
520
  # Add default region, which will be in resourcesDefaultRegion
@@ -519,7 +523,7 @@ class TerrascriptClient:
519
523
  secret_key=config["aws_secret_access_key"],
520
524
  region=config["resourcesDefaultRegion"],
521
525
  skip_region_validation=True,
522
- default_tags=self.default_tags,
526
+ default_tags={"tags": config["tags"]},
523
527
  )
524
528
 
525
529
  ts += Terraform(
@@ -802,6 +806,9 @@ class TerrascriptClient:
802
806
  config["supportedDeploymentRegions"] = account["supportedDeploymentRegions"]
803
807
  config["resourcesDefaultRegion"] = account["resourcesDefaultRegion"]
804
808
  config["terraformState"] = account["terraformState"]
809
+ config["tags"] = dict(self.default_tags) | get_aws_account_tags(
810
+ account.get("organization", None)
811
+ )
805
812
  self.configs[account_name] = config
806
813
 
807
814
  def _get_partition(self, account: str) -> str:
@@ -1056,7 +1063,9 @@ class TerrascriptClient:
1056
1063
  ignore_changes = (
1057
1064
  "all" if "all" in lifecycle.ignore_changes else lifecycle.ignore_changes
1058
1065
  )
1059
- return lifecycle.dict(by_alias=True) | {"ignore_changes": ignore_changes}
1066
+ return lifecycle.model_dump(by_alias=True) | {
1067
+ "ignore_changes": ignore_changes
1068
+ }
1060
1069
  return None
1061
1070
 
1062
1071
  def populate_additional_providers(
@@ -1071,25 +1080,15 @@ class TerrascriptClient:
1071
1080
  config = self.configs[account_name]
1072
1081
  existing_provider_aliases = {p.get("alias") for p in ts["provider"]["aws"]}
1073
1082
  if alias not in existing_provider_aliases:
1074
- if assume_role:
1075
- ts += provider.aws(
1076
- access_key=config["aws_access_key_id"],
1077
- secret_key=config["aws_secret_access_key"],
1078
- region=region,
1079
- alias=alias,
1080
- assume_role={"role_arn": assume_role},
1081
- skip_region_validation=True,
1082
- default_tags=self.default_tags,
1083
- )
1084
- else:
1085
- ts += provider.aws(
1086
- access_key=config["aws_access_key_id"],
1087
- secret_key=config["aws_secret_access_key"],
1088
- region=region,
1089
- alias=alias,
1090
- skip_region_validation=True,
1091
- default_tags=self.default_tags,
1092
- )
1083
+ ts += provider.aws(
1084
+ access_key=config["aws_access_key_id"],
1085
+ secret_key=config["aws_secret_access_key"],
1086
+ region=region,
1087
+ alias=alias,
1088
+ skip_region_validation=True,
1089
+ default_tags={"tags": config["tags"]},
1090
+ **{"assume_role": {"role_arn": assume_role}} if assume_role else {},
1091
+ )
1093
1092
 
1094
1093
  def populate_route53(
1095
1094
  self, desired_state: Iterable[Mapping[str, Any]], default_ttl: int = 300
@@ -1432,7 +1431,7 @@ class TerrascriptClient:
1432
1431
  req_account_name = req_account.name
1433
1432
  # Accepter's side of the connection - the cluster's account
1434
1433
  acc_account = accepter.account
1435
- acc_alias = self.get_provider_alias(acc_account.dict(by_alias=True))
1434
+ acc_alias = self.get_provider_alias(acc_account.model_dump(by_alias=True))
1436
1435
  acc_uid = acc_account.uid
1437
1436
  if acc_account.assume_role:
1438
1437
  acc_uid = awsh.get_account_uid_from_arn(acc_account.assume_role)
@@ -2218,14 +2217,59 @@ class TerrascriptClient:
2218
2217
  letters_and_digits = string.ascii_letters + string.digits
2219
2218
  return "".join(random.choice(letters_and_digits) for i in range(string_length))
2220
2219
 
2221
- def populate_tf_resource_s3(self, spec: ExternalResourceSpec) -> aws_s3_bucket:
2220
+ @staticmethod
2221
+ def _build_tf_resource_s3_lifecycle_rules(
2222
+ versioning: bool,
2223
+ common_values: Mapping[str, Any],
2224
+ ) -> list[dict]:
2225
+ lifecycle_rules = common_values.get("lifecycle_rules") or []
2226
+ if versioning and not any(
2227
+ "noncurrent_version_expiration" in lr for lr in lifecycle_rules
2228
+ ):
2229
+ # Add a default noncurrent object expiration rule
2230
+ # if one isn't already set
2231
+ rule = {
2232
+ "id": "expire_noncurrent_versions",
2233
+ "enabled": True,
2234
+ "noncurrent_version_expiration": {"days": 30},
2235
+ "expiration": {"expired_object_delete_marker": True},
2236
+ "abort_incomplete_multipart_upload_days": 3,
2237
+ }
2238
+ lifecycle_rules.append(rule)
2239
+
2240
+ if storage_class := common_values.get("storage_class"):
2241
+ sc = storage_class.upper()
2242
+ days = "1"
2243
+ if sc.endswith("_IA"):
2244
+ # Infrequent Access storage class has minimum 30 days
2245
+ # before transition
2246
+ days = "30"
2247
+ rule = {
2248
+ "id": sc + "_storage_class",
2249
+ "enabled": True,
2250
+ "transition": {"days": days, "storage_class": sc},
2251
+ "noncurrent_version_transition": {"days": days, "storage_class": sc},
2252
+ }
2253
+ lifecycle_rules.append(rule)
2254
+
2255
+ return lifecycle_rules
2256
+
2257
+ def _populate_tf_resource_s3_bucket(
2258
+ self,
2259
+ spec: ExternalResourceSpec,
2260
+ common_values: dict[str, Any],
2261
+ ) -> tuple[aws_s3_bucket, list[TFResource]]:
2262
+ """Create S3 bucket with configuration and notifications.
2263
+
2264
+ Creates aws_s3_bucket with versioning, encryption, lifecycle rules,
2265
+ CORS, logging, and replication. Also creates aws_s3_bucket_notification
2266
+ for SQS/SNS event notifications if configured.
2267
+ """
2222
2268
  account = spec.provisioner_name
2223
2269
  identifier = spec.identifier
2224
- common_values = self.init_values(spec)
2225
2270
  output_prefix = spec.output_prefix
2226
2271
 
2227
2272
  tf_resources: list[TFResource] = []
2228
- self.init_common_outputs(tf_resources, spec)
2229
2273
 
2230
2274
  # s3 bucket
2231
2275
  # Terraform resource reference:
@@ -2257,47 +2301,11 @@ class TerrascriptClient:
2257
2301
  request_payer = common_values.get("request_payer")
2258
2302
  if request_payer:
2259
2303
  values["request_payer"] = request_payer
2260
- lifecycle_rules = common_values.get("lifecycle_rules")
2261
- if lifecycle_rules:
2262
- # common_values['lifecycle_rules'] is a list of lifecycle_rules
2304
+ if lifecycle_rules := self._build_tf_resource_s3_lifecycle_rules(
2305
+ versioning=versioning,
2306
+ common_values=common_values,
2307
+ ):
2263
2308
  values["lifecycle_rule"] = lifecycle_rules
2264
- if versioning:
2265
- lrs = values.get("lifecycle_rule", [])
2266
- expiration_rule = False
2267
- for lr in lrs:
2268
- if "noncurrent_version_expiration" in lr:
2269
- expiration_rule = True
2270
- break
2271
- if not expiration_rule:
2272
- # Add a default noncurrent object expiration rule if
2273
- # if one isn't already set
2274
- rule = {
2275
- "id": "expire_noncurrent_versions",
2276
- "enabled": "true",
2277
- "noncurrent_version_expiration": {"days": 30},
2278
- }
2279
- if len(lrs) > 0:
2280
- lrs.append(rule)
2281
- else:
2282
- lrs = rule
2283
- sc = common_values.get("storage_class")
2284
- if sc:
2285
- sc = sc.upper()
2286
- days = "1"
2287
- if sc.endswith("_IA"):
2288
- # Infrequent Access storage class has minimum 30 days
2289
- # before transition
2290
- days = "30"
2291
- rule = {
2292
- "id": sc + "_storage_class",
2293
- "enabled": "true",
2294
- "transition": {"days": days, "storage_class": sc},
2295
- "noncurrent_version_transition": {"days": days, "storage_class": sc},
2296
- }
2297
- if values.get("lifecycle_rule"):
2298
- values["lifecycle_rule"].append(rule)
2299
- else:
2300
- values["lifecycle_rule"] = rule
2301
2309
  cors_rules = common_values.get("cors_rules")
2302
2310
  if cors_rules:
2303
2311
  # common_values['cors_rules'] is a list of cors_rules
@@ -2438,8 +2446,7 @@ class TerrascriptClient:
2438
2446
  output_name = output_prefix + "__endpoint"
2439
2447
  tf_resources.append(Output(output_name, value=endpoint))
2440
2448
 
2441
- sqs_identifier = common_values.get("sqs_identifier", None)
2442
- if sqs_identifier is not None:
2449
+ if sqs_identifier := common_values.get("sqs_identifier"):
2443
2450
  sqs_values = {"name": sqs_identifier}
2444
2451
  sqs_provider = values.get("provider")
2445
2452
  if sqs_provider:
@@ -2458,11 +2465,9 @@ class TerrascriptClient:
2458
2465
  }
2459
2466
  ],
2460
2467
  }
2461
- filter_prefix = common_values.get("filter_prefix", None)
2462
- if filter_prefix is not None:
2468
+ if filter_prefix := common_values.get("filter_prefix"):
2463
2469
  notification_values["queue"][0]["filter_prefix"] = filter_prefix
2464
- filter_suffix = common_values.get("filter_suffix", None)
2465
- if filter_suffix is not None:
2470
+ if filter_suffix := common_values.get("filter_suffix"):
2466
2471
  notification_values["queue"][0]["filter_suffix"] = filter_suffix
2467
2472
 
2468
2473
  notification_tf_resource = aws_s3_bucket_notification(
@@ -2542,21 +2547,48 @@ class TerrascriptClient:
2542
2547
  )
2543
2548
  tf_resources.append(notification_tf_resource)
2544
2549
 
2545
- bucket_policy = common_values.get("bucket_policy")
2546
- if bucket_policy:
2547
- values = {
2548
- "bucket": identifier,
2549
- "policy": bucket_policy,
2550
- "depends_on": self.get_dependencies([bucket_tf_resource]),
2551
- }
2552
- if self._multiregion_account(account):
2553
- values["provider"] = "aws." + region
2554
- bucket_policy_tf_resource = aws_s3_bucket_policy(identifier, **values)
2555
- tf_resources.append(bucket_policy_tf_resource)
2550
+ return bucket_tf_resource, tf_resources
2556
2551
 
2557
- # iam resources
2558
- # Terraform resource reference:
2559
- # https://www.terraform.io/docs/providers/aws/r/iam_access_key.html
2552
+ def _populate_tf_resource_s3_bucket_policy(
2553
+ self,
2554
+ spec: ExternalResourceSpec,
2555
+ bucket_tf_resource: aws_s3_bucket,
2556
+ policy: str,
2557
+ common_values: dict[str, Any],
2558
+ ) -> list[TFResource]:
2559
+ """Create S3 bucket policy resource.
2560
+
2561
+ Creates aws_s3_bucket_policy with the provided policy document.
2562
+ """
2563
+ account = spec.provisioner_name
2564
+ identifier = spec.identifier
2565
+ region = common_values.get("region") or self.default_regions.get(account)
2566
+ assert region # make mypy happy
2567
+
2568
+ values: dict[str, Any] = {
2569
+ "bucket": identifier,
2570
+ "policy": policy,
2571
+ "depends_on": self.get_dependencies([bucket_tf_resource]),
2572
+ }
2573
+ if self._multiregion_account(account):
2574
+ values["provider"] = "aws." + region
2575
+ bucket_policy_tf_resource = aws_s3_bucket_policy(identifier, **values)
2576
+ return [bucket_policy_tf_resource]
2577
+
2578
+ def _populate_tf_resource_s3_iam(
2579
+ self,
2580
+ spec: ExternalResourceSpec,
2581
+ bucket_tf_resource: aws_s3_bucket,
2582
+ common_values: dict[str, Any],
2583
+ ) -> list[TFResource]:
2584
+ """Create IAM resources for S3 bucket access.
2585
+
2586
+ Creates aws_iam_user, aws_iam_access_key, aws_iam_policy,
2587
+ and aws_iam_user_policy_attachment for bucket access.
2588
+ """
2589
+ identifier = spec.identifier
2590
+ output_prefix = spec.output_prefix
2591
+ tf_resources: list[TFResource] = []
2560
2592
 
2561
2593
  # iam user for bucket
2562
2594
  values = {
@@ -2614,6 +2646,32 @@ class TerrascriptClient:
2614
2646
  )
2615
2647
  tf_resources.append(tf_user_policy_attachment)
2616
2648
 
2649
+ return tf_resources
2650
+
2651
+ def populate_tf_resource_s3(self, spec: ExternalResourceSpec) -> aws_s3_bucket:
2652
+ account = spec.provisioner_name
2653
+ common_values = self.init_values(spec)
2654
+
2655
+ tf_resources: list[TFResource] = []
2656
+ self.init_common_outputs(tf_resources, spec)
2657
+
2658
+ bucket_tf_resource, bucket_resources = self._populate_tf_resource_s3_bucket(
2659
+ spec, common_values
2660
+ )
2661
+ tf_resources.extend(bucket_resources)
2662
+
2663
+ bucket_policy = common_values.get("bucket_policy")
2664
+ if bucket_policy:
2665
+ tf_resources.extend(
2666
+ self._populate_tf_resource_s3_bucket_policy(
2667
+ spec, bucket_tf_resource, bucket_policy, common_values
2668
+ )
2669
+ )
2670
+
2671
+ tf_resources.extend(
2672
+ self._populate_tf_resource_s3_iam(spec, bucket_tf_resource, common_values)
2673
+ )
2674
+
2617
2675
  self.add_resources(account, tf_resources)
2618
2676
 
2619
2677
  return bucket_tf_resource
@@ -3388,42 +3446,53 @@ class TerrascriptClient:
3388
3446
  common_values = self.init_values(spec)
3389
3447
  output_prefix = spec.output_prefix
3390
3448
 
3391
- bucket_tf_resource = self.populate_tf_resource_s3(spec)
3392
-
3393
3449
  tf_resources: list[TFResource] = []
3450
+ self.init_common_outputs(tf_resources, spec)
3451
+
3452
+ bucket_tf_resource, bucket_resources = self._populate_tf_resource_s3_bucket(
3453
+ spec, common_values
3454
+ )
3455
+ tf_resources.extend(bucket_resources)
3456
+
3457
+ tf_resources.extend(
3458
+ self._populate_tf_resource_s3_iam(spec, bucket_tf_resource, common_values)
3459
+ )
3394
3460
 
3395
3461
  # cloudfront origin access identity
3396
3462
  values = {"comment": f"{identifier}-cf-identity"}
3397
3463
  cf_oai_tf_resource = aws_cloudfront_origin_access_identity(identifier, **values)
3398
3464
  tf_resources.append(cf_oai_tf_resource)
3399
3465
 
3400
- # bucket policy for cloudfront
3401
- values_policy: dict[str, Any] = {"bucket": identifier}
3402
- policy = {
3403
- "Version": "2012-10-17",
3404
- "Statement": [
3405
- {
3406
- "Sid": "Grant access to CloudFront Origin Identity",
3407
- "Effect": "Allow",
3408
- "Principal": {"AWS": "${" + cf_oai_tf_resource.iam_arn + "}"},
3409
- "Action": "s3:GetObject",
3410
- "Resource": [
3411
- f"arn:aws:s3:::{identifier}/{enable_dir}/*"
3412
- for enable_dir in common_values.get(
3413
- "get_object_enable_dirs", []
3414
- )
3415
- ],
3416
- }
3466
+ # bucket policy for cloudfront - merge custom policy with CloudFront access statement
3467
+ cf_statement = {
3468
+ "Sid": "Grant access to CloudFront Origin Identity",
3469
+ "Effect": "Allow",
3470
+ "Principal": {"AWS": "${" + cf_oai_tf_resource.iam_arn + "}"},
3471
+ "Action": "s3:GetObject",
3472
+ "Resource": [
3473
+ f"arn:aws:s3:::{identifier}/{enable_dir}/*"
3474
+ for enable_dir in common_values.get("get_object_enable_dirs", [])
3417
3475
  ],
3418
3476
  }
3419
- values_policy["policy"] = json_dumps(policy)
3420
- values_policy["depends_on"] = self.get_dependencies([bucket_tf_resource])
3421
- region = common_values.get("region") or self.default_regions.get(account)
3422
- assert region # make mypy happy
3423
- if self._multiregion_account(account):
3424
- values_policy["provider"] = "aws." + region
3425
- bucket_policy_tf_resource = aws_s3_bucket_policy(identifier, **values_policy)
3426
- tf_resources.append(bucket_policy_tf_resource)
3477
+
3478
+ custom_bucket_policy = common_values.get("bucket_policy")
3479
+ if custom_bucket_policy:
3480
+ # if the user specifies a custom bucket policy then we merge their statements with the cloudfront origin identity policy
3481
+ if isinstance(custom_bucket_policy, str):
3482
+ custom_bucket_policy = json.loads(custom_bucket_policy)
3483
+ custom_bucket_policy.setdefault("Statement", []).append(cf_statement)
3484
+ policy = custom_bucket_policy
3485
+ else:
3486
+ policy = {
3487
+ "Version": "2012-10-17",
3488
+ "Statement": [cf_statement],
3489
+ }
3490
+
3491
+ tf_resources.extend(
3492
+ self._populate_tf_resource_s3_bucket_policy(
3493
+ spec, bucket_tf_resource, json_dumps(policy), common_values
3494
+ )
3495
+ )
3427
3496
 
3428
3497
  distribution_config = common_values.get("distribution_config", {})
3429
3498
  # aws_s3_bucket_acl
@@ -4024,6 +4093,22 @@ class TerrascriptClient:
4024
4093
  kinesis_tf_resource = aws_kinesis_stream(identifier, **kinesis_values)
4025
4094
  tf_resources.append(kinesis_tf_resource)
4026
4095
 
4096
+ # kinesis resource policy (optional)
4097
+ # Terraform resource reference:
4098
+ # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_resource_policy
4099
+ if policy := common_values.get("policy"):
4100
+ policy_identifier = f"{identifier}-policy"
4101
+ policy_values: dict[str, Any] = {
4102
+ "resource_arn": "${" + kinesis_tf_resource.arn + "}",
4103
+ "policy": policy,
4104
+ }
4105
+ if provider:
4106
+ policy_values["provider"] = provider
4107
+ kinesis_policy_tf_resource = aws_kinesis_resource_policy(
4108
+ policy_identifier, **policy_values
4109
+ )
4110
+ tf_resources.append(kinesis_policy_tf_resource)
4111
+
4027
4112
  es_identifier = common_values.get("es_identifier", None)
4028
4113
  if es_identifier:
4029
4114
  es_resource = self._find_resource_spec(
@@ -5810,6 +5895,10 @@ class TerrascriptClient:
5810
5895
  assert secret # make mypy happy
5811
5896
  secret_data = self.secret_reader.read_all(secret)
5812
5897
 
5898
+ secret_format = common_values.get("secret_format")
5899
+ if secret_format is not None:
5900
+ secret_data = self._apply_secret_format(str(secret_format), secret_data)
5901
+
5813
5902
  version_values: dict[str, Any] = {
5814
5903
  "secret_id": "${" + aws_secret_resource.id + "}",
5815
5904
  "secret_string": json_dumps(secret_data),
@@ -5833,6 +5922,66 @@ class TerrascriptClient:
5833
5922
 
5834
5923
  self.add_resources(account, tf_resources)
5835
5924
 
5925
+ @staticmethod
5926
+ def _unflatten_dotted_keys_dict(flat_dict: dict[str, str]) -> dict[str, Any]:
5927
+ """Convert a flat dictionary with dotted keys to a nested dictionary.
5928
+
5929
+ Example:
5930
+ {"db.host": "localhost", "db.port": "5432"} ->
5931
+ {"db": {"host": "localhost", "port": "5432"}}
5932
+
5933
+ Raises:
5934
+ ValueError: If there are conflicting keys (e.g., "a.b" and "a.b.c")
5935
+ """
5936
+ result: dict[str, Any] = {}
5937
+ for key, value in flat_dict.items():
5938
+ parts = key.split(".")
5939
+ current = result
5940
+ for i, part in enumerate(parts[:-1]):
5941
+ if part not in current:
5942
+ current[part] = {}
5943
+ elif not isinstance(current[part], dict):
5944
+ # Conflict: trying to traverse through a non-dict value
5945
+ conflicting_path = ".".join(parts[: i + 1])
5946
+ raise ValueError(
5947
+ f"Conflicting keys detected: '{conflicting_path}' is both a "
5948
+ f"value and a nested path in key '{key}'"
5949
+ )
5950
+ current = current[part]
5951
+
5952
+ # Check if we're trying to set a value where a dict already exists
5953
+ if parts[-1] in current and isinstance(current[parts[-1]], dict):
5954
+ raise ValueError(
5955
+ f"Conflicting keys detected: '{key}' conflicts with nested keys"
5956
+ )
5957
+
5958
+ current[parts[-1]] = value
5959
+
5960
+ return result
5961
+
5962
+ @staticmethod
5963
+ def _apply_secret_format(
5964
+ secret_format: str, secret_data: dict[str, str]
5965
+ ) -> dict[str, str]:
5966
+ # Convert flat dict with dotted keys to nested dict for Jinja2
5967
+ nested_secret_data = TerrascriptClient._unflatten_dotted_keys_dict(secret_data)
5968
+ rendered_data = process_jinja2_template(secret_format, nested_secret_data)
5969
+
5970
+ parsed_data = json.loads(rendered_data)
5971
+
5972
+ if not isinstance(parsed_data, dict):
5973
+ raise ValueError("secret_format must be a dictionary")
5974
+
5975
+ # validate secret is a dict[str, str]
5976
+ for k, v in parsed_data.items():
5977
+ if not isinstance(k, str):
5978
+ raise ValueError(f"key '{k}' is not a string")
5979
+
5980
+ if not isinstance(v, str):
5981
+ raise ValueError(f"dictionary value '{v}' under '{k}' is not a string")
5982
+
5983
+ return parsed_data
5984
+
5836
5985
  def get_commit_sha(self, repo_info: Mapping) -> str:
5837
5986
  url = repo_info["url"]
5838
5987
  ref = repo_info["ref"]
@@ -5851,7 +6000,8 @@ class TerrascriptClient:
5851
6000
  return commit.sha
5852
6001
  case "gitlab":
5853
6002
  gitlab = self.init_gitlab()
5854
- project = gitlab.get_project(url)
6003
+ if not (project := gitlab.get_project(url)):
6004
+ raise ValueError(f"could not find gitlab project for url {url}")
5855
6005
  commits = project.commits.list(ref_name=ref, per_page=1, page=1)
5856
6006
  return commits[0].id
5857
6007
  case _:
@@ -24,30 +24,24 @@ class Environment(BaseModel):
24
24
  return self.name == other
25
25
 
26
26
 
27
- class FeatureToggle(BaseModel):
27
+ class FeatureToggle(BaseModel, validate_by_name=True, validate_by_alias=True):
28
28
  name: str
29
29
  type: FeatureToggleType = FeatureToggleType.release
30
30
  description: str | None = None
31
31
  impression_data: bool = Field(False, alias="impressionData")
32
32
  environments: list[Environment]
33
33
 
34
- class Config:
35
- allow_population_by_field_name = True
36
-
37
34
  def __eq__(self, other: object) -> bool:
38
35
  if isinstance(other, FeatureToggle):
39
36
  return self.name == other.name
40
37
  return self.name == other
41
38
 
42
39
 
43
- class Project(BaseModel):
40
+ class Project(BaseModel, validate_by_name=True, validate_by_alias=True):
44
41
  pk: str = Field(alias="id")
45
42
  name: str
46
43
  feature_toggles: list[FeatureToggle] = []
47
44
 
48
- class Config:
49
- allow_population_by_field_name = True
50
-
51
45
 
52
46
  class TokenAuth(BearerTokenAuth):
53
47
  def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest:
reconcile/utils/vault.py CHANGED
@@ -6,7 +6,7 @@ import threading
6
6
  import time
7
7
  from collections.abc import Mapping
8
8
  from functools import lru_cache
9
- from typing import Any, Self, TypedDict
9
+ from typing import Any, Self
10
10
 
11
11
  import hvac
12
12
  import requests
@@ -48,13 +48,6 @@ class VaultConnectionError(Exception):
48
48
  pass
49
49
 
50
50
 
51
- class Secret(TypedDict):
52
- path: str
53
- field: str
54
- format: str | None
55
- version: str | None
56
-
57
-
58
51
  SECRET_VERSION_LATEST = "LATEST"
59
52
 
60
53
 
@@ -197,7 +190,7 @@ class VaultClient:
197
190
  self._client.auth_approle(self.role_id, self.secret_id)
198
191
 
199
192
  @retry()
200
- def read_all_with_version(self, secret: Mapping) -> tuple[Mapping, str | None]:
193
+ def read_all_with_version(self, secret: Mapping) -> tuple[dict, int | None]:
201
194
  """Returns a dictionary of keys and values in a Vault secret and the
202
195
  version of the secret, for V1 secrets, version will be None.
203
196
 
@@ -207,7 +200,7 @@ class VaultClient:
207
200
  a v2 KV engine)
208
201
  """
209
202
  secret_path = secret["path"]
210
- secret_version = secret.get("version")
203
+ secret_version = secret.get("version") or SECRET_VERSION_LATEST
211
204
 
212
205
  kv_version = self._get_mount_version_by_secret_path(secret_path)
213
206
 
@@ -250,7 +243,7 @@ class VaultClient:
250
243
 
251
244
  def __read_all_v2(
252
245
  self, path: str, version: str | None
253
- ) -> tuple[dict[str, Any], str | None]:
246
+ ) -> tuple[dict[str, Any], int]:
254
247
  path_split = path.split("/")
255
248
  mount_point = path_split[0]
256
249
  read_path = "/".join(path_split[1:])
@@ -294,7 +287,7 @@ class VaultClient:
294
287
  return secret["data"]
295
288
 
296
289
  @retry()
297
- def read(self, secret: Secret) -> Any:
290
+ def read(self, secret: Mapping[str, Any]) -> Any:
298
291
  """Returns a value of a key in a Vault secret.
299
292
 
300
293
  The input secret is a dictionary which contains the following fields: