qontract-reconcile 0.10.1rc746__py3-none-any.whl → 0.10.1rc748__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc746
3
+ Version: 0.10.1rc748
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -10,7 +10,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
10
10
  reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
11
11
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
12
12
  reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
13
- reconcile/cli.py,sha256=Fra8s6kxIuCoLF-moSto81gDFoJ6Q-WrsaRcVxR-KRI,98281
13
+ reconcile/cli.py,sha256=Vw8P6w0nPOGkMRvxpn4lR75jUeRoXLJ4R-WspWNX6Cg,98702
14
14
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
15
15
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
16
16
  reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
@@ -47,7 +47,7 @@ reconcile/jenkins_roles.py,sha256=f8ELpZY36UjoaCpR_9LijQuIMuB6a7sVLFf_H1ct9Hc,44
47
47
  reconcile/jenkins_webhooks.py,sha256=j8vhJMWcRhOdc9XzRSm0CPj84jsF3e4Syjm7r1BIsDE,1978
48
48
  reconcile/jenkins_webhooks_cleaner.py,sha256=JsN_NVPfZJwv1JtSzZXDIHUqGiefL-DRffFnDGau9aY,1539
49
49
  reconcile/jenkins_worker_fleets.py,sha256=PMNGOX0krubFjInPiFT0za0KCiWBLEcVDuXdKRd1BrE,5378
50
- reconcile/jira_permissions_validator.py,sha256=8Nl7rSc8x_AXXVRYvRlq_arR7N5pz0YM4M7Pb60RoCg,11597
50
+ reconcile/jira_permissions_validator.py,sha256=iDsFdGqB8Zv9cjIVgYFq_N3xtRCrcR5uAZmcc51D-2o,13240
51
51
  reconcile/jira_watcher.py,sha256=eyOQ92t8TFi6gogfNTO448h_h1CUyr24E0MPHc51R-o,3617
52
52
  reconcile/ldap_users.py,sha256=uEWQ0V41tN9KCZi4ZKPamjrJ6djSpdpvDBo7yJ0e7ZI,3008
53
53
  reconcile/mr_client_gateway.py,sha256=WhjMd-sIXDFCV8-rt8CEjurJ5OYB1pOD0K3o0tZRXQg,1885
@@ -618,7 +618,7 @@ reconcile/utils/helpers.py,sha256=k9svgFFZG7H5FvHYY0g5jJyvgvh2UDZxf0Ib221teag,11
618
618
  reconcile/utils/imap_client.py,sha256=byFAJATbITJPsGECSbvXBOcCnoeTUpDFiEjzOAxLm_U,1975
619
619
  reconcile/utils/instrumented_wrappers.py,sha256=eVwMoa6FCrYxLv3RML3WpZF9qKVfCTjMxphgVXG03OM,1073
620
620
  reconcile/utils/jenkins_api.py,sha256=MyJSB_S3uYf3sXnt9t03-gZNQ7tbdd7Wusv3MoF2fRc,7113
621
- reconcile/utils/jira_client.py,sha256=KjsakExjlii6lkxUlHRScBtPG4wuomJOTIoNiM-edQw,7322
621
+ reconcile/utils/jira_client.py,sha256=sCswWH-SctzPSElI9_oHv9LKuNnPfjP_FIOizxqh6nE,7693
622
622
  reconcile/utils/jjb_client.py,sha256=Pdy0dLCFvD6GPCaC0tZydYgkVJPOxYXIiwWECZaFJBU,14551
623
623
  reconcile/utils/jsonpath.py,sha256=NRpAEijKN4cMDjo7qivNPqpm0__GQQ1TiE0PBEBO45s,5572
624
624
  reconcile/utils/jump_host.py,sha256=AdwmCZYNhRe53VwV2iAsUdVyUdVtSd4REmdThJDkM5w,4973
@@ -664,7 +664,7 @@ reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM
664
664
  reconcile/utils/unleash.py,sha256=1D56CsZfE3ShDtN3IErE1T2eeIwNmxhK-yYbCotJ99E,3601
665
665
  reconcile/utils/vault.py,sha256=S0eHqvZ9N3fya1E8YDaUffEvLk_fdtpzL4rvWn6f828,14991
666
666
  reconcile/utils/vaultsecretref.py,sha256=3Ed2uBy36TzSvL0B-l4FoWQqB2SbBKDKEuUPIO608Bo,931
667
- reconcile/utils/vcs.py,sha256=uHFyYSHGpsdtiY76XVquq_5IzJTkkghBwWESeCJIeAw,8416
667
+ reconcile/utils/vcs.py,sha256=iiAWQXNftKIRoakXEOPT6ubB_ybSuInIQ6jcMxa_NKk,8558
668
668
  reconcile/utils/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
669
669
  reconcile/utils/acs/base.py,sha256=kjcxLGIMe8oLNFOxZ_bDcClFqGCL7Auwug5is0yNGbw,2555
670
670
  reconcile/utils/acs/notifiers.py,sha256=2n5blP9N1FdGLZuy3do9bpjd8NKg88kmLNNqhAGn8w4,5013
@@ -778,8 +778,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
778
778
  tools/test/test_qontract_cli.py,sha256=w2l4BHB09k1d-BGJ1jBUNCqDv7zkqYrMHojQXg-21kQ,4155
779
779
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
780
780
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
781
- qontract_reconcile-0.10.1rc746.dist-info/METADATA,sha256=wZTYzj2gWnkTP279SxzH3p9fBGaVQk9mId5meD83nUQ,2382
782
- qontract_reconcile-0.10.1rc746.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
783
- qontract_reconcile-0.10.1rc746.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
784
- qontract_reconcile-0.10.1rc746.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
785
- qontract_reconcile-0.10.1rc746.dist-info/RECORD,,
781
+ qontract_reconcile-0.10.1rc748.dist-info/METADATA,sha256=ceTfijoo-x5F6mo0WQjXgfkmBWkj6OvaecXkCj0R2-I,2382
782
+ qontract_reconcile-0.10.1rc748.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
783
+ qontract_reconcile-0.10.1rc748.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
784
+ qontract_reconcile-0.10.1rc748.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
785
+ qontract_reconcile-0.10.1rc748.dist-info/RECORD,,
reconcile/cli.py CHANGED
@@ -1144,12 +1144,26 @@ def jenkins_webhooks_cleaner(ctx):
1144
1144
  help="Throw and error in case of board permission errors. Useful for PR checks.",
1145
1145
  default=True,
1146
1146
  )
1147
+ @enable_extended_early_exit
1148
+ @extended_early_exit_cache_ttl_seconds
1149
+ @log_cached_log_output
1147
1150
  @click.pass_context
1148
- def jira_permissions_validator(ctx, exit_on_permission_errors):
1151
+ def jira_permissions_validator(
1152
+ ctx,
1153
+ exit_on_permission_errors,
1154
+ enable_extended_early_exit,
1155
+ extended_early_exit_cache_ttl_seconds,
1156
+ log_cached_log_output,
1157
+ ):
1149
1158
  import reconcile.jira_permissions_validator
1150
1159
 
1151
1160
  run_integration(
1152
- reconcile.jira_permissions_validator, ctx.obj, exit_on_permission_errors
1161
+ reconcile.jira_permissions_validator,
1162
+ ctx.obj,
1163
+ exit_on_permission_errors,
1164
+ enable_extended_early_exit=enable_extended_early_exit,
1165
+ extended_early_exit_cache_ttl_seconds=extended_early_exit_cache_ttl_seconds,
1166
+ log_cached_log_output=log_cached_log_output,
1153
1167
  )
1154
1168
 
1155
1169
 
@@ -2,14 +2,11 @@ import logging
2
2
  import sys
3
3
  from collections.abc import Callable, Iterable
4
4
  from enum import IntFlag, auto
5
- from typing import Any
5
+ from typing import Any, TypedDict
6
6
 
7
7
  from jira import JIRAError
8
8
  from pydantic import BaseModel
9
9
 
10
- from reconcile.gql_definitions.jira_permissions_validator.jira_boards_for_permissions_validator import (
11
- DEFINITION as JIRA_BOARDS_DEFINITION,
12
- )
13
10
  from reconcile.gql_definitions.jira_permissions_validator.jira_boards_for_permissions_validator import (
14
11
  JiraBoardV1,
15
12
  )
@@ -24,10 +21,17 @@ from reconcile.typed_queries.jira_settings import get_jira_settings
24
21
  from reconcile.typed_queries.jiralert_settings import get_jiralert_settings
25
22
  from reconcile.utils import gql, metrics
26
23
  from reconcile.utils.disabled_integrations import integration_is_enabled
24
+ from reconcile.utils.extended_early_exit import (
25
+ ExtendedEarlyExitRunnerResult,
26
+ extended_early_exit_run,
27
+ )
27
28
  from reconcile.utils.jira_client import JiraClient, JiraWatcherSettings
28
29
  from reconcile.utils.secret_reader import SecretReaderBase, create_secret_reader
30
+ from reconcile.utils.semver_helper import make_semver
31
+ from reconcile.utils.unleash import get_feature_toggle_state
29
32
 
30
33
  QONTRACT_INTEGRATION = "jira-permissions-validator"
34
+ QONTRACT_INTEGRATION_VERSION = make_semver(1, 0, 0)
31
35
 
32
36
  NameToIdMap = dict[str, str]
33
37
 
@@ -59,6 +63,15 @@ class ValidationError(IntFlag):
59
63
  INVALID_COMPONENT = auto()
60
64
 
61
65
 
66
+ class RunnerParams(TypedDict):
67
+ exit_on_permission_errors: bool
68
+ boards: list[JiraBoardV1]
69
+
70
+
71
+ class CacheSource(TypedDict):
72
+ boards: list
73
+
74
+
62
75
  def board_is_valid(
63
76
  jira: JiraClient,
64
77
  board: JiraBoardV1,
@@ -90,7 +103,7 @@ def board_is_valid(
90
103
  error |= ValidationError.INVALID_COMPONENT
91
104
 
92
105
  issue_type = board.issue_type if board.issue_type else default_issue_type
93
- project_issue_types = jira.project_issue_types(board.name)
106
+ project_issue_types = jira.project_issue_types()
94
107
  project_issue_types_str = [i.name for i in project_issue_types]
95
108
  if issue_type not in project_issue_types_str:
96
109
  logging.error(
@@ -192,7 +205,6 @@ def validate_boards(
192
205
  ) -> bool:
193
206
  error = False
194
207
  jira_clients: dict[str, JiraClient] = {}
195
- public_projects_cache: dict[str, list[str]] = {}
196
208
  for board in jira_boards:
197
209
  logging.debug(f"[{board.name}] checking ...")
198
210
  if board.server.server_url not in jira_clients:
@@ -204,10 +216,6 @@ def validate_boards(
204
216
  )
205
217
  jira = jira_clients[board.server.server_url]
206
218
  jira.project = board.name
207
- if board.server.server_url not in public_projects_cache:
208
- public_projects_cache[board.server.server_url] = jira.public_projects()
209
- public_projects = public_projects_cache[board.server.server_url]
210
-
211
219
  try:
212
220
  error_flags = board_is_valid(
213
221
  jira=jira,
@@ -215,7 +223,7 @@ def validate_boards(
215
223
  default_issue_type=default_issue_type,
216
224
  default_reopen_state=default_reopen_state,
217
225
  jira_server_priorities={p.name: p.id for p in jira.priorities()},
218
- public_projects=public_projects,
226
+ public_projects=jira.public_projects(),
219
227
  )
220
228
  match error_flags:
221
229
  case 0:
@@ -256,13 +264,59 @@ def get_jira_boards(query_func: Callable) -> list[JiraBoardV1]:
256
264
  ]
257
265
 
258
266
 
259
- def run(dry_run: bool, exit_on_permission_errors: bool) -> None:
267
+ def export_boards(boards: list[JiraBoardV1]) -> list[dict]:
268
+ return [board.dict() for board in boards]
269
+
270
+
271
+ def run(
272
+ dry_run: bool,
273
+ exit_on_permission_errors: bool,
274
+ enable_extended_early_exit: bool = False,
275
+ extended_early_exit_cache_ttl_seconds: int = 3600,
276
+ log_cached_log_output: bool = False,
277
+ ) -> None:
278
+ gql_api = gql.get_api()
279
+ boards = get_jira_boards(query_func=gql_api.query)
280
+ runner_params: RunnerParams = dict(
281
+ exit_on_permission_errors=exit_on_permission_errors,
282
+ boards=boards,
283
+ )
284
+ if enable_extended_early_exit and get_feature_toggle_state(
285
+ "jira-permissions-validator-extended-early-exit",
286
+ default=True,
287
+ ):
288
+ vault_settings = get_app_interface_vault_settings()
289
+ secret_reader = create_secret_reader(use_vault=vault_settings.vault)
290
+
291
+ cache_source = CacheSource(
292
+ boards=export_boards(boards),
293
+ )
294
+ extended_early_exit_run(
295
+ integration=QONTRACT_INTEGRATION,
296
+ integration_version=QONTRACT_INTEGRATION_VERSION,
297
+ # don't use `dry_run` in the cache key because this is a read-only integration
298
+ dry_run=False,
299
+ cache_source=cache_source,
300
+ shard="",
301
+ ttl_seconds=extended_early_exit_cache_ttl_seconds,
302
+ logger=logging.getLogger(),
303
+ runner=runner,
304
+ runner_params=runner_params,
305
+ secret_reader=secret_reader,
306
+ log_cached_log_output=log_cached_log_output,
307
+ )
308
+ else:
309
+ runner(**runner_params)
310
+
311
+
312
+ def runner(
313
+ exit_on_permission_errors: bool, boards: list[JiraBoardV1]
314
+ ) -> ExtendedEarlyExitRunnerResult:
260
315
  gql_api = gql.get_api()
261
316
  settings = get_jira_settings(gql_api=gql_api)
262
317
  jiralert_settings = get_jiralert_settings(query_func=gql_api.query)
263
318
  vault_settings = get_app_interface_vault_settings()
264
319
  secret_reader = create_secret_reader(use_vault=vault_settings.vault)
265
- boards = get_jira_boards(query_func=gql_api.query)
266
320
 
267
321
  with metrics.transactional_metrics("jira-boards") as metrics_container:
268
322
  error = validate_boards(
@@ -278,8 +332,8 @@ def run(dry_run: bool, exit_on_permission_errors: bool) -> None:
278
332
  if error:
279
333
  sys.exit(ExitCodes.ERROR)
280
334
 
335
+ return ExtendedEarlyExitRunnerResult(payload=export_boards(boards), applied_count=0)
336
+
281
337
 
282
338
  def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
283
- return {
284
- "boards": gql.get_api().query(JIRA_BOARDS_DEFINITION)["jira_boards"],
285
- }
339
+ return {"boards": export_boards(get_jira_boards(query_func=gql.get_api().query))}
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
3
4
  import logging
4
5
  from collections.abc import (
5
6
  Iterable,
@@ -82,6 +83,11 @@ class JiraClient:
82
83
  self.project = project
83
84
  self.jira = jira_api
84
85
 
86
+ # some caches
87
+ self.priorities = functools.lru_cache(maxsize=None)(self._priorities)
88
+ self.public_projects = functools.lru_cache(maxsize=None)(self._public_projects)
89
+ self.my_permissions = functools.lru_cache(maxsize=None)(self._my_permissions)
90
+
85
91
  def _deprecated_init(
86
92
  self, jira_board: Mapping[str, Any], settings: Optional[Mapping]
87
93
  ) -> None:
@@ -174,11 +180,12 @@ class JiraClient:
174
180
  )
175
181
  return issue
176
182
 
183
+ def _my_permissions(self, project: str) -> dict[str, Any]:
184
+ return self.jira.my_permissions(projectKey=project)["permissions"]
185
+
177
186
  def can_i(self, permission: str) -> bool:
178
187
  return bool(
179
- self.jira.my_permissions(projectKey=self.project)["permissions"][
180
- permission
181
- ]["havePermission"]
188
+ self.my_permissions(project=self.project)[permission]["havePermission"]
182
189
  )
183
190
 
184
191
  def can_create_issues(self) -> bool:
@@ -187,10 +194,10 @@ class JiraClient:
187
194
  def can_transition_issues(self) -> bool:
188
195
  return self.can_i("TRANSITION_ISSUES")
189
196
 
190
- def project_issue_types(self, project: str) -> list[IssueType]:
197
+ def project_issue_types(self) -> list[IssueType]:
191
198
  return [
192
199
  IssueType(id=t.id, name=t.name, statuses=[s.name for s in t.statuses])
193
- for t in self.jira.issue_types_for_project(project)
200
+ for t in self.jira.issue_types_for_project(self.project)
194
201
  ]
195
202
 
196
203
  def security_levels(self) -> list[SecurityLevel]:
@@ -201,7 +208,7 @@ class JiraClient:
201
208
  scheme = self.jira.project_issue_security_level_scheme(self.project)
202
209
  return [SecurityLevel(id=level.id, name=level.name) for level in scheme.levels]
203
210
 
204
- def priorities(self) -> list[Priority]:
211
+ def _priorities(self) -> list[Priority]:
205
212
  """Return a list of all available Jira priorities."""
206
213
  return [Priority(id=p.id, name=p.name) for p in self.jira.priorities()]
207
214
 
@@ -210,7 +217,7 @@ class JiraClient:
210
217
  scheme = self.jira.project_priority_scheme(self.project)
211
218
  return scheme.optionIds
212
219
 
213
- def public_projects(self) -> list[str]:
220
+ def _public_projects(self) -> list[str]:
214
221
  """Return a list of all public available projects."""
215
222
  if not self.server:
216
223
  raise RuntimeError("JiraClient.server is not set.")
reconcile/utils/vcs.py CHANGED
@@ -216,8 +216,13 @@ class VCS:
216
216
  self._app_interface_api.close(mr)
217
217
 
218
218
  def get_file_content_from_app_interface_master(self, file_path: str) -> str:
219
+ file_path = (
220
+ f"data/{file_path.lstrip('/')}"
221
+ if not file_path.startswith("data")
222
+ else file_path
223
+ )
219
224
  return self._app_interface_api.project.files.get(
220
- file_path=f"data{file_path}", ref="master"
225
+ file_path=file_path, ref="master"
221
226
  ).decode()
222
227
 
223
228
  def get_open_app_interface_merge_requests(self) -> list[ProjectMergeRequest]: