qontract-reconcile 0.10.1rc879__py3-none-any.whl → 0.10.1rc894__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 (291) hide show
  1. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/RECORD +291 -284
  3. reconcile/acs_rbac.py +1 -2
  4. reconcile/aus/advanced_upgrade_service.py +14 -14
  5. reconcile/aus/aus_label_source.py +1 -2
  6. reconcile/aus/base.py +23 -26
  7. reconcile/aus/cluster_version_data.py +4 -4
  8. reconcile/aus/models.py +2 -3
  9. reconcile/aus/version_gate_approver.py +2 -6
  10. reconcile/aus/version_gates/__init__.py +1 -3
  11. reconcile/aus/version_gates/sts_version_gate_handler.py +2 -3
  12. reconcile/aws_account_manager/integration.py +9 -14
  13. reconcile/aws_account_manager/reconciler.py +51 -1
  14. reconcile/aws_account_manager/utils.py +3 -0
  15. reconcile/aws_ami_cleanup/integration.py +3 -4
  16. reconcile/aws_iam_password_reset.py +2 -5
  17. reconcile/aws_version_sync/integration.py +2 -2
  18. reconcile/blackbox_exporter_endpoint_monitoring.py +2 -5
  19. reconcile/change_owners/approver.py +4 -5
  20. reconcile/change_owners/bundle.py +20 -22
  21. reconcile/change_owners/change_types.py +23 -24
  22. reconcile/change_owners/changes.py +13 -16
  23. reconcile/change_owners/decision.py +2 -5
  24. reconcile/change_owners/diff.py +11 -15
  25. reconcile/change_owners/self_service_roles.py +1 -2
  26. reconcile/change_owners/tester.py +7 -10
  27. reconcile/checkpoint.py +2 -5
  28. reconcile/cli.py +26 -12
  29. reconcile/closedbox_endpoint_monitoring_base.py +8 -11
  30. reconcile/cluster_deployment_mapper.py +2 -5
  31. reconcile/cna/assets/asset.py +4 -7
  32. reconcile/cna/assets/null.py +2 -5
  33. reconcile/cna/integration.py +2 -3
  34. reconcile/cna/state.py +2 -5
  35. reconcile/dashdotdb_base.py +8 -11
  36. reconcile/dashdotdb_cso.py +3 -6
  37. reconcile/dashdotdb_dora.py +10 -14
  38. reconcile/dashdotdb_dvo.py +10 -13
  39. reconcile/dashdotdb_slo.py +5 -8
  40. reconcile/database_access_manager.py +5 -6
  41. reconcile/dynatrace_token_provider/integration.py +3 -6
  42. reconcile/dynatrace_token_provider/integration_v2.py +20 -0
  43. reconcile/dynatrace_token_provider/meta.py +1 -0
  44. reconcile/external_resources/integration.py +1 -1
  45. reconcile/external_resources/manager.py +4 -4
  46. reconcile/external_resources/model.py +3 -3
  47. reconcile/external_resources/secrets_sync.py +5 -5
  48. reconcile/external_resources/state.py +5 -5
  49. reconcile/gabi_authorized_users.py +3 -6
  50. reconcile/gcr_mirror.py +1 -1
  51. reconcile/github_org.py +1 -3
  52. reconcile/github_repo_invites.py +2 -5
  53. reconcile/gitlab_housekeeping.py +7 -11
  54. reconcile/gitlab_labeler.py +1 -2
  55. reconcile/gitlab_members.py +2 -5
  56. reconcile/gitlab_permissions.py +1 -3
  57. reconcile/glitchtip/integration.py +5 -8
  58. reconcile/glitchtip_project_alerts/integration.py +57 -33
  59. reconcile/glitchtip_project_dsn/integration.py +8 -11
  60. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +6 -0
  61. reconcile/gql_definitions/fragments/aws_account_managed.py +8 -0
  62. reconcile/gql_definitions/glitchtip/glitchtip_project.py +4 -4
  63. reconcile/gql_definitions/glitchtip_project_alerts/glitchtip_project.py +27 -7
  64. reconcile/integrations_manager.py +5 -8
  65. reconcile/jenkins/types.py +5 -6
  66. reconcile/jenkins_job_builder.py +9 -12
  67. reconcile/jenkins_roles.py +1 -1
  68. reconcile/jira_watcher.py +2 -2
  69. reconcile/ldap_groups/integration.py +2 -5
  70. reconcile/ocm/types.py +21 -26
  71. reconcile/ocm_addons_upgrade_tests_trigger.py +3 -6
  72. reconcile/ocm_clusters.py +8 -8
  73. reconcile/ocm_internal_notifications/integration.py +1 -2
  74. reconcile/ocm_labels/integration.py +2 -5
  75. reconcile/ocm_machine_pools.py +11 -15
  76. reconcile/ocm_upgrade_scheduler_org_updater.py +2 -5
  77. reconcile/openshift_base.py +29 -30
  78. reconcile/openshift_groups.py +15 -20
  79. reconcile/openshift_namespace_labels.py +8 -14
  80. reconcile/openshift_namespaces.py +5 -8
  81. reconcile/openshift_network_policies.py +2 -4
  82. reconcile/openshift_resources_base.py +19 -29
  83. reconcile/openshift_saas_deploy.py +9 -10
  84. reconcile/openshift_saas_deploy_change_tester.py +7 -10
  85. reconcile/openshift_saas_deploy_trigger_base.py +4 -7
  86. reconcile/openshift_saas_deploy_trigger_cleaner.py +5 -8
  87. reconcile/openshift_saas_deploy_trigger_configs.py +1 -2
  88. reconcile/openshift_saas_deploy_trigger_images.py +1 -2
  89. reconcile/openshift_saas_deploy_trigger_moving_commits.py +1 -2
  90. reconcile/openshift_saas_deploy_trigger_upstream_jobs.py +1 -2
  91. reconcile/openshift_tekton_resources.py +7 -11
  92. reconcile/openshift_upgrade_watcher.py +10 -13
  93. reconcile/openshift_users.py +8 -11
  94. reconcile/oum/base.py +3 -4
  95. reconcile/oum/labelset.py +1 -2
  96. reconcile/oum/metrics.py +2 -2
  97. reconcile/oum/models.py +1 -2
  98. reconcile/oum/standalone.py +2 -3
  99. reconcile/prometheus_rules_tester/integration.py +6 -9
  100. reconcile/quay_membership.py +1 -2
  101. reconcile/quay_mirror.py +12 -13
  102. reconcile/quay_mirror_org.py +10 -10
  103. reconcile/queries.py +4 -7
  104. reconcile/resource_scraper.py +3 -4
  105. reconcile/rhidp/common.py +2 -2
  106. reconcile/saas_auto_promotions_manager/integration.py +5 -6
  107. reconcile/saas_auto_promotions_manager/merge_request_manager/batcher.py +1 -2
  108. reconcile/saas_auto_promotions_manager/publisher.py +5 -6
  109. reconcile/saas_auto_promotions_manager/subscriber.py +36 -15
  110. reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py +8 -0
  111. reconcile/saas_file_validator.py +2 -5
  112. reconcile/signalfx_endpoint_monitoring.py +2 -5
  113. reconcile/skupper_network/integration.py +3 -6
  114. reconcile/skupper_network/models.py +3 -5
  115. reconcile/slack_base.py +4 -7
  116. reconcile/slack_usergroups.py +15 -17
  117. reconcile/sql_query.py +5 -9
  118. reconcile/status_board.py +4 -5
  119. reconcile/statuspage/atlassian.py +14 -15
  120. reconcile/statuspage/integrations/maintenances.py +3 -3
  121. reconcile/statuspage/page.py +8 -8
  122. reconcile/statuspage/state.py +4 -5
  123. reconcile/statuspage/status.py +7 -8
  124. reconcile/templating/lib/rendering.py +8 -8
  125. reconcile/templating/renderer.py +10 -11
  126. reconcile/templating/validator.py +4 -4
  127. reconcile/terraform_aws_route53.py +3 -6
  128. reconcile/terraform_cloudflare_dns.py +9 -12
  129. reconcile/terraform_cloudflare_resources.py +9 -11
  130. reconcile/terraform_cloudflare_users.py +8 -11
  131. reconcile/terraform_init/integration.py +2 -2
  132. reconcile/terraform_repo.py +11 -14
  133. reconcile/terraform_resources.py +20 -21
  134. reconcile/terraform_tgw_attachments.py +32 -36
  135. reconcile/terraform_users.py +6 -7
  136. reconcile/terraform_vpc_resources/integration.py +6 -6
  137. reconcile/test/conftest.py +7 -10
  138. reconcile/test/fixtures.py +1 -1
  139. reconcile/test/saas_auto_promotions_manager/conftest.py +3 -2
  140. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/conftest.py +2 -2
  141. reconcile/test/test_database_access_manager.py +3 -6
  142. reconcile/test/test_gitlab_labeler.py +2 -5
  143. reconcile/test/test_jump_host.py +5 -8
  144. reconcile/test/test_ocm_machine_pools.py +1 -4
  145. reconcile/test/test_openshift_base.py +3 -6
  146. reconcile/test/test_openshift_cluster_bots.py +5 -5
  147. reconcile/test/test_openshift_namespace_labels.py +2 -3
  148. reconcile/test/test_openshift_saas_deploy_trigger_cleaner.py +2 -2
  149. reconcile/test/test_saasherder.py +9 -12
  150. reconcile/test/test_slack_base.py +4 -6
  151. reconcile/test/test_status_board.py +4 -7
  152. reconcile/test/test_terraform_tgw_attachments.py +14 -20
  153. reconcile/typed_queries/alerting_services_settings.py +1 -2
  154. reconcile/typed_queries/app_interface_custom_messages.py +2 -3
  155. reconcile/typed_queries/app_interface_deadmanssnitch_settings.py +1 -3
  156. reconcile/typed_queries/app_interface_repo_url.py +1 -2
  157. reconcile/typed_queries/app_interface_state_settings.py +1 -3
  158. reconcile/typed_queries/app_interface_vault_settings.py +1 -2
  159. reconcile/typed_queries/aws_vpc_requests.py +1 -3
  160. reconcile/typed_queries/aws_vpcs.py +1 -3
  161. reconcile/typed_queries/clusters.py +2 -4
  162. reconcile/typed_queries/clusters_minimal.py +1 -3
  163. reconcile/typed_queries/clusters_with_dms.py +1 -3
  164. reconcile/typed_queries/dynatrace_environments.py +14 -0
  165. reconcile/typed_queries/external_resources.py +3 -4
  166. reconcile/typed_queries/pagerduty_instances.py +1 -2
  167. reconcile/typed_queries/repos.py +2 -3
  168. reconcile/typed_queries/reserved_networks.py +1 -3
  169. reconcile/typed_queries/saas_files.py +49 -59
  170. reconcile/typed_queries/slo_documents.py +1 -3
  171. reconcile/typed_queries/status_board.py +3 -7
  172. reconcile/typed_queries/tekton_pipeline_providers.py +1 -2
  173. reconcile/typed_queries/terraform_namespaces.py +1 -2
  174. reconcile/typed_queries/terraform_tgw_attachments/aws_accounts.py +1 -3
  175. reconcile/utils/acs/base.py +2 -3
  176. reconcile/utils/acs/notifiers.py +3 -3
  177. reconcile/utils/acs/policies.py +3 -3
  178. reconcile/utils/aggregated_list.py +1 -1
  179. reconcile/utils/amtool.py +1 -2
  180. reconcile/utils/aws_api.py +28 -31
  181. reconcile/utils/aws_api_typed/account.py +23 -0
  182. reconcile/utils/aws_api_typed/api.py +20 -9
  183. reconcile/utils/binary.py +1 -3
  184. reconcile/utils/clusterhealth/providerbase.py +1 -2
  185. reconcile/utils/clusterhealth/telemeter.py +2 -2
  186. reconcile/utils/deadmanssnitch_api.py +1 -2
  187. reconcile/utils/disabled_integrations.py +4 -6
  188. reconcile/utils/environ.py +1 -1
  189. reconcile/utils/expiration.py +3 -7
  190. reconcile/utils/external_resource_spec.py +3 -4
  191. reconcile/utils/external_resources.py +4 -7
  192. reconcile/utils/filtering.py +1 -2
  193. reconcile/utils/git.py +3 -9
  194. reconcile/utils/git_secrets.py +5 -5
  195. reconcile/utils/github_api.py +5 -9
  196. reconcile/utils/gitlab_api.py +2 -3
  197. reconcile/utils/glitchtip/client.py +2 -4
  198. reconcile/utils/glitchtip/models.py +8 -11
  199. reconcile/utils/gql.py +26 -35
  200. reconcile/utils/grouping.py +1 -3
  201. reconcile/utils/imap_client.py +2 -5
  202. reconcile/utils/internal_groups/client.py +1 -2
  203. reconcile/utils/internal_groups/models.py +8 -9
  204. reconcile/utils/jenkins_api.py +4 -4
  205. reconcile/utils/jinja2/extensions.py +1 -1
  206. reconcile/utils/jinja2/filters.py +4 -4
  207. reconcile/utils/jinja2/utils.py +16 -16
  208. reconcile/utils/jira_client.py +10 -11
  209. reconcile/utils/jjb_client.py +14 -17
  210. reconcile/utils/jobcontroller/controller.py +5 -5
  211. reconcile/utils/jobcontroller/models.py +2 -2
  212. reconcile/utils/jsonpath.py +4 -5
  213. reconcile/utils/jump_host.py +7 -8
  214. reconcile/utils/keycloak.py +3 -7
  215. reconcile/utils/ldap_client.py +2 -3
  216. reconcile/utils/lean_terraform_client.py +13 -17
  217. reconcile/utils/membershipsources/app_interface_resolver.py +1 -1
  218. reconcile/utils/membershipsources/models.py +19 -22
  219. reconcile/utils/metrics.py +13 -15
  220. reconcile/utils/mr/base.py +7 -11
  221. reconcile/utils/mr/glitchtip_access_reporter.py +2 -2
  222. reconcile/utils/mr/notificator.py +1 -2
  223. reconcile/utils/oc.py +38 -38
  224. reconcile/utils/oc_connection_parameters.py +24 -25
  225. reconcile/utils/oc_filters.py +2 -3
  226. reconcile/utils/oc_map.py +9 -15
  227. reconcile/utils/ocm/addons.py +7 -10
  228. reconcile/utils/ocm/base.py +38 -39
  229. reconcile/utils/ocm/clusters.py +6 -9
  230. reconcile/utils/ocm/label_sources.py +1 -2
  231. reconcile/utils/ocm/labels.py +3 -6
  232. reconcile/utils/ocm/ocm.py +11 -14
  233. reconcile/utils/ocm/products.py +1 -3
  234. reconcile/utils/ocm/search_filters.py +16 -17
  235. reconcile/utils/ocm/service_log.py +2 -3
  236. reconcile/utils/ocm/sre_capability_labels.py +4 -8
  237. reconcile/utils/ocm/subscriptions.py +1 -3
  238. reconcile/utils/ocm/syncsets.py +2 -4
  239. reconcile/utils/ocm/upgrades.py +5 -9
  240. reconcile/utils/ocm_base_client.py +13 -16
  241. reconcile/utils/openshift_resource.py +5 -11
  242. reconcile/utils/output.py +2 -3
  243. reconcile/utils/pagerduty_api.py +4 -5
  244. reconcile/utils/prometheus.py +2 -2
  245. reconcile/utils/promotion_state.py +4 -5
  246. reconcile/utils/promtool.py +2 -8
  247. reconcile/utils/quay_api.py +12 -22
  248. reconcile/utils/raw_github_api.py +3 -5
  249. reconcile/utils/rosa/rosa_cli.py +6 -6
  250. reconcile/utils/rosa/session.py +6 -7
  251. reconcile/utils/runtime/desired_state_diff.py +3 -8
  252. reconcile/utils/runtime/environment.py +4 -7
  253. reconcile/utils/runtime/integration.py +4 -4
  254. reconcile/utils/runtime/meta.py +1 -2
  255. reconcile/utils/runtime/runner.py +7 -10
  256. reconcile/utils/runtime/sharding.py +22 -27
  257. reconcile/utils/saasherder/interfaces.py +63 -69
  258. reconcile/utils/saasherder/models.py +30 -35
  259. reconcile/utils/saasherder/saasherder.py +39 -54
  260. reconcile/utils/secret_reader.py +17 -19
  261. reconcile/utils/slack_api.py +15 -17
  262. reconcile/utils/smtp_client.py +1 -2
  263. reconcile/utils/sqs_gateway.py +1 -3
  264. reconcile/utils/state.py +1 -2
  265. reconcile/utils/terraform/config_client.py +4 -5
  266. reconcile/utils/terraform_client.py +12 -8
  267. reconcile/utils/terrascript/cloudflare_client.py +4 -10
  268. reconcile/utils/terrascript/cloudflare_resources.py +10 -13
  269. reconcile/utils/terrascript/models.py +2 -3
  270. reconcile/utils/terrascript/resources.py +1 -2
  271. reconcile/utils/terrascript_aws_client.py +50 -38
  272. reconcile/utils/unleash/client.py +4 -7
  273. reconcile/utils/unleash/server.py +2 -2
  274. reconcile/utils/vault.py +8 -11
  275. reconcile/utils/vaultsecretref.py +2 -3
  276. reconcile/utils/vcs.py +7 -8
  277. reconcile/vault_replication.py +4 -8
  278. reconcile/vpc_peerings_validator.py +4 -9
  279. release/version.py +6 -7
  280. tools/app_interface_reporter.py +2 -2
  281. tools/cli_commands/gpg_encrypt.py +3 -6
  282. tools/cli_commands/systems_and_tools.py +4 -7
  283. tools/qontract_cli.py +105 -17
  284. tools/saas_promotion_state/__init__.py +0 -0
  285. tools/saas_promotion_state/saas_promotion_state.py +105 -0
  286. tools/template_validation.py +1 -1
  287. tools/test/conftest.py +45 -6
  288. tools/test/test_saas_promotion_state.py +187 -0
  289. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/WHEEL +0 -0
  290. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/entry_points.txt +0 -0
  291. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/top_level.txt +0 -0
tools/qontract_cli.py CHANGED
@@ -9,16 +9,13 @@ import re
9
9
  import sys
10
10
  from collections import defaultdict
11
11
  from datetime import (
12
+ UTC,
12
13
  datetime,
13
14
  timedelta,
14
- timezone,
15
15
  )
16
16
  from operator import itemgetter
17
17
  from statistics import median
18
- from typing import (
19
- Any,
20
- Optional,
21
- )
18
+ from typing import Any
22
19
 
23
20
  import boto3
24
21
  import click
@@ -59,6 +56,14 @@ from reconcile.cli import (
59
56
  config_file,
60
57
  use_jump_host,
61
58
  )
59
+ from reconcile.external_resources.manager import (
60
+ FLAG_RESOURCE_MANAGED_BY_ERV2,
61
+ setup_factories,
62
+ )
63
+ from reconcile.external_resources.model import (
64
+ ExternalResourcesInventory,
65
+ load_module_inventory,
66
+ )
62
67
  from reconcile.gql_definitions.advanced_upgrade_service.aus_clusters import (
63
68
  query as aus_clusters_query,
64
69
  )
@@ -75,6 +80,11 @@ from reconcile.typed_queries.app_interface_vault_settings import (
75
80
  get_app_interface_vault_settings,
76
81
  )
77
82
  from reconcile.typed_queries.clusters import get_clusters
83
+ from reconcile.typed_queries.external_resources import (
84
+ get_modules,
85
+ get_namespaces,
86
+ get_settings,
87
+ )
78
88
  from reconcile.typed_queries.saas_files import get_saas_files
79
89
  from reconcile.typed_queries.slo_documents import get_slo_documents
80
90
  from reconcile.typed_queries.status_board import get_status_board
@@ -352,8 +362,8 @@ def get_upgrade_policies_data(
352
362
 
353
363
  def soaking_str(
354
364
  soaking: dict[str, Any],
355
- upgrade_policy: Optional[AbstractUpgradePolicy],
356
- upgradeable_version: Optional[str],
365
+ upgrade_policy: AbstractUpgradePolicy | None,
366
+ upgradeable_version: str | None,
357
367
  ) -> str:
358
368
  if upgrade_policy:
359
369
  upgrade_version = upgrade_policy.version
@@ -764,9 +774,9 @@ def ocm_addon_upgrade_policies(ctx: click.core.Context) -> None:
764
774
  @click.pass_context
765
775
  def sd_app_sre_alert_report(
766
776
  ctx: click.core.Context,
767
- days: Optional[int],
768
- from_timestamp: Optional[int],
769
- to_timestamp: Optional[int],
777
+ days: int | None,
778
+ from_timestamp: int | None,
779
+ to_timestamp: int | None,
770
780
  ) -> None:
771
781
  import tools.sd_app_sre_alert_report as report
772
782
 
@@ -1407,9 +1417,7 @@ def rosa_create_cluster_command(ctx, cluster_name):
1407
1417
  @click.argument("jumphost_hostname", required=False)
1408
1418
  @click.argument("cluster_name", required=False)
1409
1419
  @click.pass_context
1410
- def sshuttle_command(
1411
- ctx, jumphost_hostname: Optional[str], cluster_name: Optional[str]
1412
- ):
1420
+ def sshuttle_command(ctx, jumphost_hostname: str | None, cluster_name: str | None):
1413
1421
  jumphosts_query_data = queries.get_jumphosts(hostname=jumphost_hostname)
1414
1422
  jumphosts = jumphosts_query_data.jumphosts or []
1415
1423
  for jh in jumphosts:
@@ -2358,7 +2366,7 @@ def ec2_jenkins_workers(ctx, aws_access_key_id, aws_secret_access_key, aws_regio
2358
2366
  client = boto3.client("autoscaling")
2359
2367
  ec2 = boto3.resource("ec2")
2360
2368
  results = []
2361
- now = datetime.now(timezone.utc)
2369
+ now = datetime.now(UTC)
2362
2370
  DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
2363
2371
  columns = [
2364
2372
  "type",
@@ -2511,7 +2519,7 @@ def alerts(ctx, file_path):
2511
2519
  case _:
2512
2520
  return BIG_NUMBER
2513
2521
 
2514
- with open(file_path, "r", encoding="locale") as f:
2522
+ with open(file_path, encoding="locale") as f:
2515
2523
  content = json.loads(f.read())
2516
2524
 
2517
2525
  columns = [
@@ -2605,7 +2613,7 @@ def osd_component_versions(ctx):
2605
2613
  @get.command()
2606
2614
  @click.pass_context
2607
2615
  def maintenances(ctx):
2608
- now = datetime.now(timezone.utc)
2616
+ now = datetime.now(UTC)
2609
2617
  maintenances = maintenances_gql.query(gql.get_api().query).maintenances or []
2610
2618
  data = [
2611
2619
  {
@@ -3474,7 +3482,7 @@ def saas_dev(ctx, app_name=None, saas_file_name=None, env_name=None) -> None:
3474
3482
  @click.option("--app-name", default=None, help="app to act on.")
3475
3483
  @click.pass_context
3476
3484
  def saas_targets(
3477
- ctx, saas_file_name: Optional[str] = None, app_name: Optional[str] = None
3485
+ ctx, saas_file_name: str | None = None, app_name: str | None = None
3478
3486
  ) -> None:
3479
3487
  """Resolve namespaceSelectors and print all resulting targets of a saas file."""
3480
3488
  console = Console()
@@ -3663,6 +3671,46 @@ def gpg_encrypt(
3663
3671
  ).execute()
3664
3672
 
3665
3673
 
3674
+ @root.command()
3675
+ @click.option("--channel", help="the channel that state is part of")
3676
+ @click.option("--sha", help="the commit sha we want state for")
3677
+ @environ(["APP_INTERFACE_STATE_BUCKET"])
3678
+ def get_promotion_state(channel: str, sha: str):
3679
+ from tools.saas_promotion_state.saas_promotion_state import (
3680
+ SaasPromotionState,
3681
+ )
3682
+
3683
+ promotion_state = SaasPromotionState.create(promotion_state=None, saas_files=None)
3684
+ for publisher_id, state in promotion_state.get(channel=channel, sha=sha).items():
3685
+ print()
3686
+ if not state:
3687
+ print(f"No state found for {publisher_id=}")
3688
+ else:
3689
+ print(f"State for {publisher_id=}:")
3690
+ print(state)
3691
+
3692
+
3693
+ @root.command()
3694
+ @click.option("--channel", help="the channel that state is part of")
3695
+ @click.option("--sha", help="the commit sha we want state for")
3696
+ @click.option("--publisher-id", help="the publisher id we want state for")
3697
+ @environ(["APP_INTERFACE_STATE_BUCKET"])
3698
+ def mark_promotion_state_successful(channel: str, sha: str, publisher_id: str):
3699
+ from tools.saas_promotion_state.saas_promotion_state import (
3700
+ SaasPromotionState,
3701
+ )
3702
+
3703
+ promotion_state = SaasPromotionState.create(promotion_state=None, saas_files=None)
3704
+ print(f"Current states for {publisher_id=}")
3705
+ print(promotion_state.get(channel=channel, sha=sha).get(publisher_id, None))
3706
+ print()
3707
+ print("Pushing new state ...")
3708
+ promotion_state.set_successful(channel=channel, sha=sha, publisher_uid=publisher_id)
3709
+ print()
3710
+ print(f"New state for {publisher_id=}")
3711
+ print(promotion_state.get(channel=channel, sha=sha).get(publisher_id, None))
3712
+
3713
+
3666
3714
  @root.command()
3667
3715
  @click.option("--change-type-name")
3668
3716
  @click.option("--role-name")
@@ -3770,5 +3818,45 @@ def remove(ctx, sso_client_vault_secret_path: str):
3770
3818
  )
3771
3819
 
3772
3820
 
3821
+ @root.group()
3822
+ @click.pass_context
3823
+ def external_resources(ctx):
3824
+ """External resources commands"""
3825
+
3826
+
3827
+ @external_resources.command()
3828
+ @click.argument("provision-provider", required=True)
3829
+ @click.argument("provisioner", required=True)
3830
+ @click.argument("provider", required=True)
3831
+ @click.argument("identifier", required=True)
3832
+ @click.pass_context
3833
+ def get_input(
3834
+ ctx, provision_provider: str, provisioner: str, provider: str, identifier: str
3835
+ ):
3836
+ """Gets the input data for an external resource asset. Input data is what is used
3837
+ in the Reconciliation Job to manage the resource.
3838
+
3839
+ e.g: qontract-reconcile --config=<config> external-resources get-input aws app-sre-stage rds dashdotdb-stage
3840
+ """
3841
+ namespaces = [ns for ns in get_namespaces() if ns.external_resources]
3842
+ er_inventory = ExternalResourcesInventory(namespaces)
3843
+
3844
+ spec = er_inventory.get_inventory_spec(
3845
+ provision_provider=provision_provider,
3846
+ provisioner=provisioner,
3847
+ provider=provider,
3848
+ identifier=identifier,
3849
+ )
3850
+ vault_settings = get_app_interface_vault_settings()
3851
+ secret_reader = create_secret_reader(use_vault=vault_settings.vault)
3852
+ er_settings = get_settings()[0]
3853
+ m_inventory = load_module_inventory(get_modules())
3854
+ factories = setup_factories(er_settings, m_inventory, er_inventory, secret_reader)
3855
+ f = factories.get_factory(spec.provision_provider)
3856
+ resource = f.create_external_resource(spec)
3857
+ f.validate_external_resource(resource)
3858
+ print(resource.json(exclude={"data": {FLAG_RESOURCE_MANAGED_BY_ERV2}}))
3859
+
3860
+
3773
3861
  if __name__ == "__main__":
3774
3862
  root() # pylint: disable=no-value-for-parameter
File without changes
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from reconcile.openshift_saas_deploy import (
6
+ QONTRACT_INTEGRATION as OPENSHIFT_SAAS_DEPLOY,
7
+ )
8
+ from reconcile.typed_queries.app_interface_vault_settings import (
9
+ get_app_interface_vault_settings,
10
+ )
11
+ from reconcile.typed_queries.saas_files import SaasFile, get_saas_files
12
+ from reconcile.utils.promotion_state import PromotionData, PromotionState
13
+ from reconcile.utils.secret_reader import create_secret_reader
14
+ from reconcile.utils.state import init_state
15
+
16
+
17
+ class SaasPromotionStateException(Exception):
18
+ pass
19
+
20
+
21
+ class SaasPromotionStateMissingException(Exception):
22
+ pass
23
+
24
+
25
+ class SaasPromotionState:
26
+ def __init__(
27
+ self, promotion_state: PromotionState, saas_files: Iterable[SaasFile]
28
+ ) -> None:
29
+ self._promotion_state = promotion_state
30
+ self._saas_files = saas_files
31
+
32
+ def _publisher_ids_for_channel(
33
+ self, channel: str, saas_files: Iterable[SaasFile]
34
+ ) -> list[str]:
35
+ publisher_uids: list[str] = []
36
+ for saas_file in saas_files:
37
+ for resource_template in saas_file.resource_templates:
38
+ for target in resource_template.targets:
39
+ if not target.promotion:
40
+ continue
41
+ for publish_channel in target.promotion.publish or []:
42
+ if publish_channel == channel:
43
+ publisher_uids.append(
44
+ target.uid(
45
+ parent_saas_file_name=saas_file.name,
46
+ parent_resource_template_name=resource_template.name,
47
+ )
48
+ )
49
+ return publisher_uids
50
+
51
+ def get(self, channel: str, sha: str) -> dict[str, PromotionData | None]:
52
+ return {
53
+ publisher_id: self._promotion_state.get_promotion_data(
54
+ sha=sha,
55
+ channel=channel,
56
+ use_cache=False,
57
+ target_uid=publisher_id,
58
+ pre_check_sha_exists=False,
59
+ )
60
+ for publisher_id in self._publisher_ids_for_channel(
61
+ channel=channel, saas_files=self._saas_files
62
+ )
63
+ }
64
+
65
+ def set_successful(self, channel: str, sha: str, publisher_uid: str) -> None:
66
+ current_data = self._promotion_state.get_promotion_data(
67
+ sha=sha,
68
+ channel=channel,
69
+ target_uid=publisher_uid,
70
+ use_cache=False,
71
+ pre_check_sha_exists=False,
72
+ )
73
+
74
+ if not current_data:
75
+ raise SaasPromotionStateMissingException(
76
+ f"No promotion state in S3 for given {publisher_uid=} {sha=} {channel=}"
77
+ )
78
+
79
+ if current_data.success:
80
+ raise SaasPromotionStateException(
81
+ f"The current promotion state is already marked successful for given {publisher_uid=} {sha=} {channel=}",
82
+ current_data,
83
+ )
84
+
85
+ current_data.success = True
86
+ self._promotion_state.publish_promotion_data(
87
+ data=current_data, sha=sha, channel=channel, target_uid=publisher_uid
88
+ )
89
+
90
+ @staticmethod
91
+ def create(
92
+ promotion_state: PromotionState | None, saas_files: Iterable[SaasFile] | None
93
+ ) -> SaasPromotionState:
94
+ if not promotion_state:
95
+ vault_settings = get_app_interface_vault_settings()
96
+ secret_reader = create_secret_reader(use_vault=vault_settings.vault)
97
+ saas_deploy_state = init_state(
98
+ integration=OPENSHIFT_SAAS_DEPLOY, secret_reader=secret_reader
99
+ )
100
+ promotion_state = PromotionState(state=saas_deploy_state)
101
+ if not saas_files:
102
+ saas_files = get_saas_files()
103
+ return SaasPromotionState(
104
+ promotion_state=promotion_state, saas_files=saas_files
105
+ )
@@ -27,7 +27,7 @@ def load_clean_yaml(path: str) -> dict:
27
27
 
28
28
  def load_yaml(to_load: str) -> dict:
29
29
  ruamel_instance = create_ruamel_instance()
30
- with open(to_load, "r", encoding="utf-8") as file:
30
+ with open(to_load, encoding="utf-8") as file:
31
31
  return ruamel_instance.load(file)
32
32
 
33
33
 
tools/test/conftest.py CHANGED
@@ -1,16 +1,17 @@
1
1
  from collections.abc import (
2
2
  Callable,
3
+ Iterable,
4
+ Mapping,
3
5
  MutableMapping,
4
6
  )
5
- from typing import (
6
- Any,
7
- Optional,
8
- )
7
+ from pathlib import Path
8
+ from typing import Any
9
9
 
10
10
  import pytest
11
11
  from pydantic import BaseModel
12
12
  from pydantic.error_wrappers import ValidationError
13
13
 
14
+ from reconcile.typed_queries.saas_files import SaasFile
14
15
  from reconcile.utils.models import data_default_none
15
16
 
16
17
 
@@ -18,17 +19,55 @@ class GQLClassFactoryError(Exception):
18
19
  pass
19
20
 
20
21
 
22
+ @pytest.fixture
23
+ def saas_files_builder(
24
+ gql_class_factory: Callable[[type[SaasFile], Mapping], SaasFile],
25
+ ) -> Callable[[Iterable[MutableMapping]], list[SaasFile]]:
26
+ def builder(data: Iterable[MutableMapping]) -> list[SaasFile]:
27
+ for d in data:
28
+ if "app" not in d:
29
+ d["app"] = {}
30
+ if "pipelinesProvider" not in d:
31
+ d["pipelinesProvider"] = {}
32
+ if "managedResourceTypes" not in d:
33
+ d["managedResourceTypes"] = []
34
+ if "imagePatterns" not in d:
35
+ d["imagePatterns"] = []
36
+ for rt in d.get("resourceTemplates", []):
37
+ for t in rt.get("targets", []):
38
+ ns = t["namespace"]
39
+ if "name" not in ns:
40
+ ns["name"] = "some_name"
41
+ if "environment" not in ns:
42
+ ns["environment"] = {}
43
+ if "app" not in ns:
44
+ ns["app"] = {}
45
+ if "cluster" not in ns:
46
+ ns["cluster"] = {}
47
+ return [gql_class_factory(SaasFile, d) for d in data]
48
+
49
+ return builder
50
+
51
+
52
+ @pytest.fixture
53
+ def fx() -> Callable:
54
+ def _fx(name: str) -> str:
55
+ return (Path(__file__).parent / "fixtures" / name).read_text()
56
+
57
+ return _fx
58
+
59
+
21
60
  @pytest.fixture
22
61
  def gql_class_factory() -> (
23
62
  Callable[
24
- [type[BaseModel], Optional[MutableMapping[str, Any]]],
63
+ [type[BaseModel], MutableMapping[str, Any] | None],
25
64
  BaseModel,
26
65
  ]
27
66
  ):
28
67
  """Create a GQL class from a fixture and set default values to None."""
29
68
 
30
69
  def _gql_class_factory(
31
- klass: type[BaseModel], data: Optional[MutableMapping[str, Any]] = None
70
+ klass: type[BaseModel], data: MutableMapping[str, Any] | None = None
32
71
  ) -> BaseModel:
33
72
  try:
34
73
  return klass(**data_default_none(klass, data or {}))
@@ -0,0 +1,187 @@
1
+ from collections.abc import (
2
+ Callable,
3
+ Iterable,
4
+ Mapping,
5
+ )
6
+ from unittest.mock import (
7
+ create_autospec,
8
+ )
9
+
10
+ from pytest import raises
11
+
12
+ from reconcile.typed_queries.saas_files import SaasFile
13
+ from reconcile.utils.promotion_state import PromotionData, PromotionState
14
+ from tools.saas_promotion_state.saas_promotion_state import (
15
+ SaasPromotionState,
16
+ SaasPromotionStateException,
17
+ SaasPromotionStateMissingException,
18
+ )
19
+
20
+
21
+ def test_get_saas_promotion_state(
22
+ saas_files_builder: Callable[[Iterable[Mapping]], list[SaasFile]],
23
+ ) -> None:
24
+ saas_files = saas_files_builder([
25
+ {
26
+ "path": "/saas1.yml",
27
+ "name": "saas_1",
28
+ "resourceTemplates": [
29
+ {
30
+ "name": "template_1",
31
+ "url": "repo1/url",
32
+ "targets": [
33
+ {
34
+ "ref": "main",
35
+ "namespace": {"path": "/namespace1.yml"},
36
+ "promotion": {
37
+ "publish": ["channel-a"],
38
+ },
39
+ }
40
+ ],
41
+ }
42
+ ],
43
+ },
44
+ {
45
+ "path": "/saas2.yml",
46
+ "name": "saas_2",
47
+ "resourceTemplates": [
48
+ {
49
+ "name": "template_2",
50
+ "url": "repo2/url",
51
+ "targets": [
52
+ {
53
+ "ref": "main",
54
+ "namespace": {"path": "/namespace2.yml"},
55
+ "promotion": {
56
+ "publish": ["channel-b"],
57
+ "subscribe": ["channel-a"],
58
+ },
59
+ },
60
+ {
61
+ "ref": "main",
62
+ "namespace": {"path": "/namespace3.yml"},
63
+ },
64
+ ],
65
+ }
66
+ ],
67
+ },
68
+ ])
69
+
70
+ expected = PromotionData(
71
+ check_in="test1",
72
+ saas_file="test2",
73
+ success=True,
74
+ target_config_hash="test3",
75
+ )
76
+ promotion_state = create_autospec(spec=PromotionState)
77
+ promotion_state.get_promotion_data.return_value = expected
78
+ saas_promotion_state = SaasPromotionState.create(
79
+ promotion_state=promotion_state, saas_files=saas_files
80
+ )
81
+ result = saas_promotion_state.get(channel="channel-a", sha="main")
82
+
83
+ assert result == {"616af45d7fad7f4eea8d52b8b5e8a058cef82ab0": expected}
84
+ promotion_state.get_promotion_data.assert_called_once_with(
85
+ sha="main",
86
+ channel="channel-a",
87
+ use_cache=False,
88
+ target_uid="616af45d7fad7f4eea8d52b8b5e8a058cef82ab0",
89
+ pre_check_sha_exists=False,
90
+ )
91
+
92
+
93
+ def test_set_saas_promotion_state_success(
94
+ saas_files_builder: Callable[[Iterable[Mapping]], list[SaasFile]],
95
+ ) -> None:
96
+ saas_files = saas_files_builder([{"resourceTemplates": []}])
97
+
98
+ current_data = PromotionData(
99
+ check_in="test1",
100
+ saas_file="test2",
101
+ success=False,
102
+ target_config_hash="test3",
103
+ )
104
+ promotion_state = create_autospec(spec=PromotionState)
105
+ promotion_state.get_promotion_data.return_value = current_data
106
+ saas_promotion_state = SaasPromotionState.create(
107
+ promotion_state=promotion_state, saas_files=saas_files
108
+ )
109
+ saas_promotion_state.set_successful(
110
+ channel="test-channel", sha="test-sha", publisher_uid="test-uid"
111
+ )
112
+
113
+ promotion_state.get_promotion_data.assert_called_once_with(
114
+ sha="test-sha",
115
+ channel="test-channel",
116
+ use_cache=False,
117
+ target_uid="test-uid",
118
+ pre_check_sha_exists=False,
119
+ )
120
+ promotion_state.publish_promotion_data.assert_called_once_with(
121
+ data=PromotionData(
122
+ check_in="test1",
123
+ saas_file="test2",
124
+ success=True,
125
+ target_config_hash="test3",
126
+ ),
127
+ channel="test-channel",
128
+ sha="test-sha",
129
+ target_uid="test-uid",
130
+ )
131
+
132
+
133
+ def test_set_saas_promotion_state_missing(
134
+ saas_files_builder: Callable[[Iterable[Mapping]], list[SaasFile]],
135
+ ) -> None:
136
+ saas_files = saas_files_builder([{"resourceTemplates": []}])
137
+ promotion_state = create_autospec(spec=PromotionState)
138
+ promotion_state.get_promotion_data.return_value = None
139
+ saas_promotion_state = SaasPromotionState.create(
140
+ promotion_state=promotion_state, saas_files=saas_files
141
+ )
142
+
143
+ with raises(SaasPromotionStateMissingException):
144
+ saas_promotion_state.set_successful(
145
+ channel="test-channel", sha="test-sha", publisher_uid="test-uid"
146
+ )
147
+
148
+ promotion_state.get_promotion_data.assert_called_once_with(
149
+ sha="test-sha",
150
+ channel="test-channel",
151
+ use_cache=False,
152
+ target_uid="test-uid",
153
+ pre_check_sha_exists=False,
154
+ )
155
+ promotion_state.publish_promotion_data.assert_not_called()
156
+
157
+
158
+ def test_set_saas_promotion_state_already_successful(
159
+ saas_files_builder: Callable[[Iterable[Mapping]], list[SaasFile]],
160
+ ) -> None:
161
+ saas_files = saas_files_builder([{"resourceTemplates": []}])
162
+
163
+ current_data = PromotionData(
164
+ check_in="test1",
165
+ saas_file="test2",
166
+ success=True,
167
+ target_config_hash="test3",
168
+ )
169
+ promotion_state = create_autospec(spec=PromotionState)
170
+ promotion_state.get_promotion_data.return_value = current_data
171
+ saas_promotion_state = SaasPromotionState.create(
172
+ promotion_state=promotion_state, saas_files=saas_files
173
+ )
174
+
175
+ with raises(SaasPromotionStateException):
176
+ saas_promotion_state.set_successful(
177
+ channel="test-channel", sha="test-sha", publisher_uid="test-uid"
178
+ )
179
+
180
+ promotion_state.get_promotion_data.assert_called_once_with(
181
+ sha="test-sha",
182
+ channel="test-channel",
183
+ use_cache=False,
184
+ target_uid="test-uid",
185
+ pre_check_sha_exists=False,
186
+ )
187
+ promotion_state.publish_promotion_data.assert_not_called()