qontract-reconcile 0.10.1rc354__py3-none-any.whl → 0.10.1rc355__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.1rc354
3
+ Version: 0.10.1rc355
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
@@ -69,8 +69,8 @@ reconcile/openshift_resources.py,sha256=kwsY5cko7udEKNlhL2oKiKv_5wzEw9wmmwROE016
69
69
  reconcile/openshift_resources_base.py,sha256=K56KXgxxnAsVRvqSMVTW47_ebCsxYSc0BsViwZcRS6k,44409
70
70
  reconcile/openshift_rolebindings.py,sha256=1k0o3hb3ZhhlbUjc8cP7IjKFux0oZApT8kLT8Y-pvqI,6579
71
71
  reconcile/openshift_routes.py,sha256=fXvuPSjcjVw1X3j2EQvUAdbOepmIFdKk-M3qP8QzPiw,1075
72
- reconcile/openshift_saas_deploy.py,sha256=2V3uPZib7KqG3dZ29Yl2BPV1Ao9oRoBV1XLDVBj7lbw,10699
73
- reconcile/openshift_saas_deploy_change_tester.py,sha256=zosvOIY50MHwLvq_NBX2K4eKcpNx-ENKZf1tSLg09Po,8893
72
+ reconcile/openshift_saas_deploy.py,sha256=In3hQfSCni3T5P1vP_j3bzEO2thLoQuxz4HrMSPjyIc,10761
73
+ reconcile/openshift_saas_deploy_change_tester.py,sha256=spWjxapC-u4TrCAsz1Q6_297QwfrIRx19oqz2bRPQn0,8907
74
74
  reconcile/openshift_saas_deploy_trigger_base.py,sha256=UEKWAJo6cN3Nml89tzJzbnpkJ7efOnFDf9Wfz9_tBdg,14325
75
75
  reconcile/openshift_saas_deploy_trigger_cleaner.py,sha256=tcvziJdw5lgJbbogk0-wKT2aYCFP99sL4qTSfau4otY,2971
76
76
  reconcile/openshift_saas_deploy_trigger_configs.py,sha256=uWzUV5D5CW0frdi1ys7BObNg-rA-VZKlefd4TD_Z-pY,959
@@ -456,7 +456,7 @@ reconcile/typed_queries/namespaces.py,sha256=vItPrn7sfcHOix-VvkzQkf54_ljzI_ymyxh
456
456
  reconcile/typed_queries/namespaces_minimal.py,sha256=rUtqNQ0ORXXUTQfnpsMURymAJ4gYtE77V-Lb3LiJFEY,278
457
457
  reconcile/typed_queries/pagerduty_instances.py,sha256=QCHqEAakiH6eSob0Pnnn3IBd8Ga0zpEp1Z6Qu3v2uH4,733
458
458
  reconcile/typed_queries/repos.py,sha256=RKBsf7IDS6NsXTtXxJ9Ol9G3bxG9sr3vW9QQ2bahEHo,512
459
- reconcile/typed_queries/saas_files.py,sha256=bqokMGJIbIMV6EZbio8uIecuqCrk9-MS8jBzN3q3HL4,12209
459
+ reconcile/typed_queries/saas_files.py,sha256=BuDZf83hv6ItJMqBEWBOF14XNyeEwr4qMkEWsA1fTK8,13990
460
460
  reconcile/typed_queries/smtp.py,sha256=aSLglYa5bHKmlGwKkxq2RZqyMWuAf0a4S_mOuhDa084,542
461
461
  reconcile/typed_queries/status_board.py,sha256=jsisR46kuHJM0VOPRQinnZigFNo51lHyvy-Q6Fdhs94,1673
462
462
  reconcile/typed_queries/tekton_pipeline_providers.py,sha256=2mpHBdsNPQB94tw0H9aenGuqj8EEjYolQ03YEq1CpiY,546
@@ -607,8 +607,8 @@ tools/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
607
607
  tools/test/test_qontract_cli.py,sha256=awwTHEc2DWlykuqGIYM0WOBoSL0KRnOraCLk3C7izis,1401
608
608
  tools/test/test_sd_app_sre_alert_report.py,sha256=JeLhgzpKCPgLvptwg_4ZvJHLVWKNG1T5845HXTkMBxA,1826
609
609
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
610
- qontract_reconcile-0.10.1rc354.dist-info/METADATA,sha256=GO-ppoBN3oRal8GaVSD77eqjxDpLNKwg8QjiCuoBIuI,2347
611
- qontract_reconcile-0.10.1rc354.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
612
- qontract_reconcile-0.10.1rc354.dist-info/entry_points.txt,sha256=ErVY2Jp-0Rtuq5KOtMlW5yvna4nIEuc_1YbEdEdcy9o,301
613
- qontract_reconcile-0.10.1rc354.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
614
- qontract_reconcile-0.10.1rc354.dist-info/RECORD,,
610
+ qontract_reconcile-0.10.1rc355.dist-info/METADATA,sha256=OpYBE-4PcUg9vnQAfecLU0U3GXQITpJb672__LKMp4Q,2347
611
+ qontract_reconcile-0.10.1rc355.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
612
+ qontract_reconcile-0.10.1rc355.dist-info/entry_points.txt,sha256=ErVY2Jp-0Rtuq5KOtMlW5yvna4nIEuc_1YbEdEdcy9o,301
613
+ qontract_reconcile-0.10.1rc355.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
614
+ qontract_reconcile-0.10.1rc355.dist-info/RECORD,,
@@ -20,7 +20,7 @@ from reconcile.typed_queries.app_interface_vault_settings import (
20
20
  )
21
21
  from reconcile.typed_queries.saas_files import (
22
22
  SaasFile,
23
- get_saas_files,
23
+ SaasFileList,
24
24
  get_saasherder_settings,
25
25
  )
26
26
  from reconcile.utils.defer import defer
@@ -100,15 +100,17 @@ def run(
100
100
  env_name: Optional[str] = None,
101
101
  trigger_integration: Optional[str] = None,
102
102
  trigger_reason: Optional[str] = None,
103
- all_saas_files: Optional[list[SaasFile]] = None,
103
+ saas_file_list: Optional[SaasFileList] = None,
104
104
  defer: Optional[Callable] = None,
105
105
  ) -> None:
106
106
  vault_settings = get_app_interface_vault_settings()
107
107
  secret_reader = create_secret_reader(use_vault=vault_settings.vault)
108
108
 
109
- if not all_saas_files:
110
- all_saas_files = get_saas_files()
111
- saas_files = get_saas_files(saas_file_name, env_name)
109
+ if not saas_file_list:
110
+ saas_file_list = SaasFileList()
111
+ all_saas_files = saas_file_list.saas_files
112
+ saas_files = saas_file_list.where(name=saas_file_name, env_name=env_name)
113
+
112
114
  if not saas_files:
113
115
  logging.error("no saas files found")
114
116
  raise RuntimeError("no saas files found")
@@ -18,7 +18,7 @@ from reconcile.gql_definitions.common.saas_files import (
18
18
  from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
19
19
  from reconcile.typed_queries.saas_files import (
20
20
  SaasFile,
21
- get_saas_files,
21
+ SaasFileList,
22
22
  )
23
23
  from reconcile.utils import gql
24
24
  from reconcile.utils.gitlab_api import GitLabApi
@@ -58,7 +58,7 @@ def osd_run_wrapper(
58
58
  dry_run: bool,
59
59
  available_thread_pool_size: int,
60
60
  use_jump_host: bool,
61
- all_saas_files: Optional[list[SaasFile]],
61
+ saas_file_list: Optional[SaasFileList],
62
62
  ) -> int:
63
63
  saas_file_name, env_name = spec
64
64
  exit_code = 0
@@ -69,7 +69,7 @@ def osd_run_wrapper(
69
69
  use_jump_host=use_jump_host,
70
70
  saas_file_name=saas_file_name,
71
71
  env_name=env_name,
72
- all_saas_files=all_saas_files,
72
+ saas_file_list=saas_file_list,
73
73
  )
74
74
  except SystemExit as e:
75
75
  exit_code = e.code if isinstance(e.code, int) else 1
@@ -214,10 +214,10 @@ def run(
214
214
  )
215
215
  # find the differences in saas-file state
216
216
  comparison_saas_file_state = collect_state(
217
- get_saas_files(query_func=comparison_gql_api.query)
217
+ SaasFileList(query_func=comparison_gql_api.query).saas_files
218
218
  )
219
- all_saas_files = get_saas_files()
220
- desired_saas_file_state = collect_state(all_saas_files)
219
+ saas_file_list = SaasFileList()
220
+ desired_saas_file_state = collect_state(saas_file_list.saas_files)
221
221
  # compare dicts against dicts which is much faster than comparing BaseModel objects
222
222
  comparison_saas_file_state_dicts = [s.dict() for s in comparison_saas_file_state]
223
223
  saas_file_state_diffs = [
@@ -249,7 +249,7 @@ def run(
249
249
  dry_run=dry_run,
250
250
  available_thread_pool_size=available_thread_pool_size,
251
251
  use_jump_host=use_jump_host,
252
- all_saas_files=all_saas_files,
252
+ saas_file_list=saas_file_list,
253
253
  )
254
254
 
255
255
  if [ec for ec in exit_codes if ec]:
@@ -1,6 +1,7 @@
1
1
  import hashlib
2
2
  import json
3
3
  from collections.abc import Callable
4
+ from threading import Lock
4
5
  from typing import (
5
6
  Any,
6
7
  Optional,
@@ -8,7 +9,6 @@ from typing import (
8
9
  )
9
10
 
10
11
  from jsonpath_ng.exceptions import JsonPathParserError
11
- from jsonpath_ng.ext import parser
12
12
  from pydantic import (
13
13
  BaseModel,
14
14
  Extra,
@@ -51,6 +51,7 @@ from reconcile.utils.exceptions import (
51
51
  AppInterfaceSettingsError,
52
52
  ParameterError,
53
53
  )
54
+ from reconcile.utils.jsonpath import parse_jsonpath
54
55
 
55
56
 
56
57
  class SaasResourceTemplateTarget(ConfiguredBaseModel):
@@ -141,51 +142,164 @@ class SaasFile(ConfiguredBaseModel):
141
142
  self_service_roles: Optional[list[RoleV1]] = Field(..., alias="selfServiceRoles")
142
143
 
143
144
 
144
- def get_namespaces_by_selector(
145
- namespaces: list[SaasTargetNamespace],
146
- namespace_selector: SaasResourceTemplateTargetNamespaceSelectorV1,
147
- ) -> list[SaasTargetNamespace]:
148
- # json representation of all the namespaces to filter on
149
- # remove all the None values to simplify the jsonpath expressions
150
- namespaces_as_dict = {
151
- "namespace": [ns.dict(by_alias=True, exclude_none=True) for ns in namespaces]
152
- }
153
-
154
- def _get_namespace_by_cluster_and_name(
155
- cluster_name: str, name: str
156
- ) -> SaasTargetNamespace:
157
- for ns in namespaces:
158
- if ns.cluster.name == cluster_name and ns.name == name:
159
- return ns
160
- # this should never ever happen - just make mypy happy
161
- raise RuntimeError(f"namespace '{name}' not found in cluster '{cluster_name}'")
162
-
163
- filtered_namespaces: dict[str, Any] = {}
164
-
165
- try:
145
+ class SaasFileList:
146
+ def __init__(
147
+ self,
148
+ name: Optional[str] = None,
149
+ query_func: Optional[Callable] = None,
150
+ namespaces: Optional[list[SaasTargetNamespace]] = None,
151
+ ) -> None:
152
+ # query_func and namespaces are optional args mostly used in tests
153
+ if not query_func:
154
+ query_func = gql.get_api().query
155
+ if not namespaces:
156
+ namespaces = namespaces_query(query_func).namespaces or []
157
+ self.namespaces = namespaces
158
+ self.cluster_namespaces = {
159
+ (ns.cluster.name, ns.name): ns for ns in self.namespaces
160
+ }
161
+
162
+ self._init_caches()
163
+
164
+ self.saas_files_v2 = saas_files_query(query_func).saas_files or []
165
+ if name:
166
+ self.saas_files_v2 = [sf for sf in self.saas_files_v2 if sf.name == name]
167
+ self.saas_files = self._resolve_namespace_selectors()
168
+
169
+ def _init_caches(self) -> None:
170
+ self._namespaces_as_dict_cache: Optional[dict[str, list[Any]]] = None
171
+ self._namespaces_as_dict_lock = Lock()
172
+ self._matching_namespaces_cache: dict[str, Any] = {}
173
+ self._matching_namespaces_lock = Lock()
174
+
175
+ def _resolve_namespace_selectors(self) -> list[SaasFile]:
176
+ saas_files: list[SaasFile] = []
177
+ # resolve namespaceSelectors to real namespaces
178
+ for sfv2 in self.saas_files_v2:
179
+ for rt_gql in sfv2.resource_templates:
180
+ for target_gql in rt_gql.targets[:]:
181
+ # either namespace or namespaceSelector must be set
182
+ if target_gql.namespace and target_gql.namespace_selector:
183
+ raise ParameterError(
184
+ f"SaasFile {sfv2.name}: namespace and namespaceSelector are mutually exclusive"
185
+ )
186
+ if not target_gql.provider:
187
+ target_gql.provider = "static"
188
+
189
+ if (
190
+ target_gql.namespace_selector
191
+ and target_gql.provider != "dynamic"
192
+ ):
193
+ raise ParameterError(
194
+ f"SaasFile {sfv2.name}: namespaceSelector can only be used with 'provider: dynamic'"
195
+ )
196
+ if (
197
+ target_gql.namespace_selector
198
+ and target_gql.provider == "dynamic"
199
+ ):
200
+ rt_gql.targets.remove(target_gql)
201
+ rt_gql.targets += self.create_targets_for_namespace_selector(
202
+ target_gql, target_gql.namespace_selector
203
+ )
204
+ # convert SaasFileV2 (with optional resource_templates.targets.namespace field)
205
+ # to SaasFile (with required resource_templates.targets.namespace field)
206
+ saas_files.append(SaasFile(**export_model(sfv2)))
207
+ return saas_files
208
+
209
+ def create_targets_for_namespace_selector(
210
+ self,
211
+ target: SaasResourceTemplateTargetV2,
212
+ namespace_selector: SaasResourceTemplateTargetNamespaceSelectorV1,
213
+ ) -> list[SaasResourceTemplateTargetV2]:
214
+ targets = []
215
+ for namespace in self.get_namespaces_by_selector(namespace_selector):
216
+ target_dict = export_model(target)
217
+ target_dict["namespace"] = export_model(namespace)
218
+ targets.append(SaasResourceTemplateTargetV2(**target_dict))
219
+ return targets
220
+
221
+ def _get_namespaces_as_dict(self) -> dict[str, list[Any]]:
222
+ # json representation of all the namespaces to filter on
223
+ # remove all the None values to simplify the jsonpath expressions
224
+ if self._namespaces_as_dict_cache is None:
225
+ with self._namespaces_as_dict_lock:
226
+ self._namespaces_as_dict_cache = {
227
+ "namespace": [
228
+ ns.dict(by_alias=True, exclude_none=True)
229
+ for ns in self.namespaces
230
+ ]
231
+ }
232
+ return self._namespaces_as_dict_cache
233
+
234
+ def _matching_namespaces(self, selector: str) -> Any:
235
+ if selector not in self._matching_namespaces_cache:
236
+ with self._matching_namespaces_lock:
237
+ namespaces_as_dict = self._get_namespaces_as_dict()
238
+ try:
239
+ self._matching_namespaces_cache[selector] = parse_jsonpath(
240
+ selector
241
+ ).find(namespaces_as_dict)
242
+ except JsonPathParserError as e:
243
+ raise ParameterError(
244
+ f"Invalid jsonpath expression in namespaceSelector '{selector}' :{e}"
245
+ )
246
+
247
+ return self._matching_namespaces_cache[selector]
248
+
249
+ def get_namespaces_by_selector(
250
+ self, namespace_selector: SaasResourceTemplateTargetNamespaceSelectorV1
251
+ ) -> list[SaasTargetNamespace]:
252
+ filtered_namespaces: dict[tuple[str, str], Any] = {}
253
+
166
254
  for include in namespace_selector.json_path_selectors.include:
167
- for match in parser.parse(include).find(namespaces_as_dict):
255
+ for match in self._matching_namespaces(include):
168
256
  cluster_name = match.value["cluster"]["name"]
169
257
  ns_name = match.value["name"]
170
- filtered_namespaces[
171
- f"{cluster_name}-{ns_name}"
172
- ] = _get_namespace_by_cluster_and_name(cluster_name, ns_name)
173
- except JsonPathParserError as e:
174
- raise ParameterError(
175
- f"Invalid jsonpath expression in namespaceSelector '{include}' :{e}"
176
- )
258
+ filtered_namespaces[(cluster_name, ns_name)] = self.cluster_namespaces[
259
+ (cluster_name, ns_name)
260
+ ]
177
261
 
178
- try:
179
262
  for exclude in namespace_selector.json_path_selectors.exclude or []:
180
- for match in parser.parse(exclude).find(namespaces_as_dict):
263
+ for match in self._matching_namespaces(exclude):
181
264
  cluster_name = match.value["cluster"]["name"]
182
265
  ns_name = match.value["name"]
183
- filtered_namespaces.pop(f"{cluster_name}-{ns_name}", None)
184
- except JsonPathParserError as e:
185
- raise ParameterError(
186
- f"Invalid jsonpath expression in namespaceSelector '{exclude}' :{e}"
187
- )
188
- return list(filtered_namespaces.values())
266
+ filtered_namespaces.pop((cluster_name, ns_name), None)
267
+
268
+ return list(filtered_namespaces.values())
269
+
270
+ def where(
271
+ self,
272
+ name: Optional[str] = None,
273
+ env_name: Optional[str] = None,
274
+ app_name: Optional[str] = None,
275
+ ) -> list[SaasFile]:
276
+ if name is None and env_name is None and app_name is None:
277
+ return self.saas_files
278
+
279
+ if name == "" or env_name == "" or app_name == "":
280
+ return []
281
+
282
+ filtered: list[SaasFile] = []
283
+ for saas_file in self.saas_files[:]:
284
+ if name and saas_file.name != name:
285
+ continue
286
+
287
+ if app_name and saas_file.app.name != app_name:
288
+ continue
289
+
290
+ sf = saas_file.copy(deep=True)
291
+ if env_name:
292
+ for rt in sf.resource_templates[:]:
293
+ for target in rt.targets[:]:
294
+ if target.namespace.environment.name != env_name:
295
+ rt.targets.remove(target)
296
+ if not rt.targets:
297
+ sf.resource_templates.remove(rt)
298
+ if not sf.resource_templates:
299
+ continue
300
+ filtered.append(sf)
301
+
302
+ return filtered
189
303
 
190
304
 
191
305
  def convert_parameters_to_json_string(root: dict[str, Any]) -> dict[str, Any]:
@@ -207,92 +321,19 @@ def export_model(model: BaseModel) -> dict[str, Any]:
207
321
  return convert_parameters_to_json_string(model.dict(by_alias=True))
208
322
 
209
323
 
210
- def create_targets_for_namespace_selector(
211
- target: SaasResourceTemplateTargetV2,
212
- namespaces: list[SaasTargetNamespace],
213
- namespace_selector: SaasResourceTemplateTargetNamespaceSelectorV1,
214
- ) -> list[SaasResourceTemplateTargetV2]:
215
- targets = []
216
- for namespace in get_namespaces_by_selector(namespaces, namespace_selector):
217
- target_dict = export_model(target)
218
- target_dict["namespace"] = export_model(namespace)
219
- targets.append(SaasResourceTemplateTargetV2(**target_dict))
220
- return targets
221
-
222
-
223
324
  def get_saas_files(
224
325
  name: Optional[str] = None,
225
326
  env_name: Optional[str] = None,
226
327
  app_name: Optional[str] = None,
227
328
  query_func: Optional[Callable] = None,
228
329
  namespaces: Optional[list[SaasTargetNamespace]] = None,
330
+ saas_file_list: Optional[SaasFileList] = None,
229
331
  ) -> list[SaasFile]:
230
- if not query_func:
231
- query_func = gql.get_api().query
232
- data = saas_files_query(query_func)
233
- saas_files: list[SaasFile] = []
234
- if not namespaces:
235
- namespaces = namespaces_query(query_func).namespaces or []
236
-
237
- data_saas_files = list(data.saas_files or [])
238
- if name:
239
- data_saas_files = [sf for sf in data_saas_files if sf.name == name]
240
- # resolve namespaceSelectors to real namespaces
241
- for saas_file_gql in data_saas_files:
242
- for rt_gql in saas_file_gql.resource_templates:
243
- for target_gql in rt_gql.targets[:]:
244
- # either namespace or namespaceSelector must be set
245
- if target_gql.namespace and target_gql.namespace_selector:
246
- raise ParameterError(
247
- f"SaasFile {saas_file_gql.name}: namespace and namespaceSelector are mutually exclusive"
248
- )
249
- if not target_gql.provider:
250
- target_gql.provider = "static"
251
-
252
- if (
253
- target_gql.namespace_selector
254
- and not target_gql.provider == "dynamic"
255
- ):
256
- raise ParameterError(
257
- f"SaasFile {saas_file_gql.name}: namespaceSelector can only be used with 'provider: dynamic'"
258
- )
259
- if target_gql.namespace_selector and target_gql.provider == "dynamic":
260
- rt_gql.targets.remove(target_gql)
261
- rt_gql.targets += create_targets_for_namespace_selector(
262
- target_gql, namespaces, target_gql.namespace_selector
263
- )
264
- # convert SaasFileV2 (with optional resource_templates.targets.namespace field)
265
- # to SaasFile (with required resource_templates.targets.namespace field)
266
- saas_files.append(SaasFile(**export_model(saas_file_gql)))
267
-
268
- if name is None and env_name is None and app_name is None:
269
- return saas_files
270
- if name == "" or env_name == "" or app_name == "":
271
- return []
272
-
273
- for saas_file in saas_files[:]:
274
- if name:
275
- if saas_file.name != name:
276
- saas_files.remove(saas_file)
277
- continue
278
-
279
- if env_name:
280
- for rt in saas_file.resource_templates[:]:
281
- for target in rt.targets[:]:
282
- if target.namespace.environment.name != env_name:
283
- rt.targets.remove(target)
284
- if not rt.targets:
285
- saas_file.resource_templates.remove(rt)
286
- if not saas_file.resource_templates:
287
- saas_files.remove(saas_file)
288
- continue
289
-
290
- if app_name:
291
- if saas_file.app.name != app_name:
292
- saas_files.remove(saas_file)
293
- continue
294
-
295
- return saas_files
332
+ if not saas_file_list:
333
+ saas_file_list = SaasFileList(
334
+ name=name, query_func=query_func, namespaces=namespaces
335
+ )
336
+ return saas_file_list.where(env_name=env_name, app_name=app_name)
296
337
 
297
338
 
298
339
  def get_saasherder_settings(