qontract-reconcile 0.10.2.dev485__py3-none-any.whl → 0.10.2.dev494__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.
@@ -0,0 +1,42 @@
1
+ from collections.abc import Callable
2
+
3
+ from pydantic import BaseModel
4
+ from qontract_utils.vcs import Provider
5
+
6
+ from reconcile.gql_definitions.common.vcs import query
7
+ from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
8
+ from reconcile.utils import gql
9
+
10
+
11
+ class Vcs(BaseModel):
12
+ name: str
13
+ url: str
14
+ default: bool = False
15
+ token: VaultSecret
16
+ provider: Provider
17
+
18
+
19
+ def get_vcs_instances(query_func: Callable | None = None) -> list[Vcs]:
20
+ if not query_func:
21
+ query_func = gql.get_api().query
22
+ data = query(query_func=query_func)
23
+ vcs = [
24
+ Vcs(
25
+ name=gh_org.name,
26
+ url=gh_org.url,
27
+ default=gh_org.default or False,
28
+ token=gh_org.token,
29
+ provider=Provider.GITHUB,
30
+ )
31
+ for gh_org in data.gh_orgs or []
32
+ ]
33
+ vcs += [
34
+ Vcs(
35
+ name=gl_instance.name,
36
+ url=gl_instance.url,
37
+ token=gl_instance.token,
38
+ provider=Provider.GITLAB,
39
+ )
40
+ for gl_instance in data.gl_instances or []
41
+ ]
42
+ return vcs
@@ -10,12 +10,16 @@ from typing import (
10
10
  Optional,
11
11
  TypeVar,
12
12
  )
13
+ from urllib.parse import urlparse
13
14
 
15
+ from httpx import Response
14
16
  from pydantic import BaseModel
17
+ from qontract_api_client.client import AuthenticatedClient
15
18
 
16
19
  from reconcile.typed_queries.app_interface_vault_settings import (
17
20
  get_app_interface_vault_settings,
18
21
  )
22
+ from reconcile.utils.config import get_config
19
23
  from reconcile.utils.secret_reader import (
20
24
  SecretReaderBase,
21
25
  create_secret_reader,
@@ -238,7 +242,58 @@ class QontractReconcileIntegration[RunParamsTypeVar: RunParams](ABC):
238
242
  )
239
243
 
240
244
 
245
+ class QontractReconcileApiIntegration[RunParamsTypeVar: RunParams](ABC):
246
+ """
247
+ The base class for all integrations using the Qontract API.
248
+ """
249
+
250
+ def __init__(self, params: RunParamsTypeVar) -> None:
251
+ self.params: RunParamsTypeVar = params
252
+
253
+ @property
254
+ @abstractmethod
255
+ def name(self) -> str: ...
256
+
257
+ @staticmethod
258
+ async def _raise_on_4xx_5xx(response: Response) -> None:
259
+ response.raise_for_status()
260
+
261
+ @property
262
+ def qontract_api_client(self) -> AuthenticatedClient:
263
+ """
264
+ Returns the qontract-api client.
265
+ """
266
+ config = get_config()
267
+ return AuthenticatedClient(
268
+ base_url=urlparse(config["qontract-api"]["server"]).geturl(),
269
+ token=config["qontract-api"].get("token", ""),
270
+ httpx_args={
271
+ "event_hooks": {"response": [self._raise_on_4xx_5xx]},
272
+ },
273
+ )
274
+
275
+ @property
276
+ def secret_manager_url(self) -> str:
277
+ """
278
+ Returns the configured secret manager URL.
279
+ """
280
+ config = get_config()
281
+ return config["vault"]["server"]
282
+
283
+ @abstractmethod
284
+ async def async_run(self, dry_run: bool) -> None:
285
+ """
286
+ The `async_run` function of a QontractReconcileIntegration is the asynchronous
287
+ entry point to its actual functionality. It is obliged to honor the `dry_run`
288
+ argument and not perform any changes to the system if it is set to `True`.
289
+ At the same time the integration should progress as far as possible in the dry-run
290
+ mode to highlight any issues that would have prevented it from running in non-dry-run
291
+ mode.
292
+ """
293
+
294
+
241
295
  RUN_FUNCTION = "run"
296
+ ASYNC_RUN_FUNCTION = "async_run"
242
297
  NAME_FIELD = "QONTRACT_INTEGRATION"
243
298
  EARLY_EXIT_DESIRED_STATE_FUNCTION = "early_exit_desired_state"
244
299
  DESIRED_STATE_SHARD_CONFIG_FUNCTION = "desired_state_shard_config"
@@ -308,3 +363,47 @@ class ModuleBasedQontractReconcileIntegration(
308
363
 
309
364
  def run(self, dry_run: bool) -> None:
310
365
  self.params.module.run(dry_run, *self.params.args, **self.params.kwargs)
366
+
367
+ async def async_run(self, dry_run: bool) -> None:
368
+ await self.params.module.async_run(
369
+ dry_run, *self.params.args, **self.params.kwargs
370
+ )
371
+
372
+
373
+ class ModuleBasedQontractReconcileApiIntegration(
374
+ QontractReconcileApiIntegration[ModuleArgsKwargsRunParams]
375
+ ):
376
+ """
377
+ Since most integrations are implemented as modules, this class provides a
378
+ wrapper around a module that implements the `QontractReconcileIntegration`
379
+ interface. This way such module based integrations can be used as if they
380
+ were instances of the `QontractReconcileIntegration` class.
381
+ """
382
+
383
+ def __init__(self, params: ModuleArgsKwargsRunParams):
384
+ super().__init__(params)
385
+ # self.name # run to check if the name can be extracted from the module
386
+ if not self._integration_supports(NAME_FIELD):
387
+ raise NotImplementedError(f"Integration has no {NAME_FIELD} field")
388
+ if not self._integration_supports(ASYNC_RUN_FUNCTION):
389
+ raise NotImplementedError(
390
+ f"Integration has no {ASYNC_RUN_FUNCTION}() function"
391
+ )
392
+
393
+ def _integration_supports(self, func_name: str) -> bool:
394
+ """
395
+ Verifies, that an integration supports a specific function.
396
+ todo: more thorough verification of the functions signature would be required.
397
+ """
398
+ return func_name in dir(self.params.module)
399
+
400
+ @property
401
+ def name(self) -> str:
402
+ if self._integration_supports(NAME_FIELD):
403
+ return self.params.module.QONTRACT_INTEGRATION.replace("_", "-")
404
+ raise NotImplementedError("Integration missing QONTRACT_INTEGRATION.")
405
+
406
+ async def async_run(self, dry_run: bool) -> None:
407
+ await self.params.module.async_run(
408
+ dry_run, *self.params.args, **self.params.kwargs
409
+ )
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import logging
2
3
  import sys
3
4
  from dataclasses import dataclass
@@ -15,6 +16,7 @@ from reconcile.utils.runtime.desired_state_diff import (
15
16
  build_desired_state_diff,
16
17
  )
17
18
  from reconcile.utils.runtime.integration import (
19
+ QontractReconcileApiIntegration,
18
20
  QontractReconcileIntegration,
19
21
  RunParams,
20
22
  )
@@ -28,7 +30,7 @@ class IntegrationRunConfiguration:
28
30
  Holds all required context and configuration for an integration run.
29
31
  """
30
32
 
31
- integration: QontractReconcileIntegration
33
+ integration: QontractReconcileIntegration | QontractReconcileApiIntegration
32
34
  valdiate_schemas: bool
33
35
  """
34
36
  Whether to fail an integration if it queries schemas it is not allowed to.
@@ -66,10 +68,18 @@ class IntegrationRunConfiguration:
66
68
  """
67
69
 
68
70
  def main_bundle_desired_state(self) -> dict[str, Any] | None:
71
+ if not isinstance(self.integration, QontractReconcileIntegration):
72
+ raise RuntimeError(
73
+ "main_bundle_desired_state is only available for QontractReconcileIntegration"
74
+ )
69
75
  self.switch_to_main_bundle()
70
76
  return self.integration.get_early_exit_desired_state()
71
77
 
72
78
  def comparison_bundle_desired_state(self) -> dict[str, Any] | None:
79
+ if not isinstance(self.integration, QontractReconcileIntegration):
80
+ raise RuntimeError(
81
+ "comparison_bundle_desired_state is only available for QontractReconcileIntegration"
82
+ )
73
83
  self.switch_to_comparison_bundle()
74
84
  data = self.integration.get_early_exit_desired_state()
75
85
  self.switch_to_main_bundle()
@@ -133,6 +143,7 @@ def get_desired_state_diff(
133
143
  logging.exception("Failed to fetch desired state for current bundle")
134
144
  return None
135
145
 
146
+ assert isinstance(run_cfg.integration, QontractReconcileIntegration)
136
147
  return build_desired_state_diff(
137
148
  run_cfg.integration.get_desired_state_shard_config()
138
149
  if run_cfg.check_only_affected_shards
@@ -157,16 +168,21 @@ def run_integration_cfg(run_cfg: IntegrationRunConfiguration) -> None:
157
168
 
158
169
 
159
170
  def _integration_wet_run[RunParamsTypeVar: RunParams](
160
- integration: QontractReconcileIntegration[RunParamsTypeVar],
171
+ integration: QontractReconcileIntegration[RunParamsTypeVar]
172
+ | QontractReconcileApiIntegration[RunParamsTypeVar],
161
173
  ) -> None:
162
174
  """
163
175
  Runs an integration in wet mode, i.e. not in dry-run mode.
164
176
  """
165
- integration.run(False)
177
+ if isinstance(integration, QontractReconcileIntegration):
178
+ integration.run(False)
179
+ else:
180
+ asyncio.run(integration.async_run(False))
166
181
 
167
182
 
168
183
  def _integration_dry_run[RunParamsTypeVar: RunParams](
169
- integration: QontractReconcileIntegration[RunParamsTypeVar],
184
+ integration: QontractReconcileIntegration[RunParamsTypeVar]
185
+ | QontractReconcileApiIntegration[RunParamsTypeVar],
170
186
  desired_state_diff: DesiredStateDiff | None,
171
187
  ) -> None:
172
188
  """
@@ -189,7 +205,8 @@ def _integration_dry_run[RunParamsTypeVar: RunParams](
189
205
  # we can still try to run the integration in sharded mode on the
190
206
  # affected shards only
191
207
  if (
192
- integration.supports_sharded_dry_run_mode()
208
+ isinstance(integration, QontractReconcileIntegration)
209
+ and integration.supports_sharded_dry_run_mode()
193
210
  and not integration.params_have_shard_info() # already running in sharded mode?
194
211
  and desired_state_diff
195
212
  and desired_state_diff.affected_shards
@@ -201,6 +218,7 @@ def _integration_dry_run[RunParamsTypeVar: RunParams](
201
218
  sharded_integration = integration.build_integration_instance_for_shard(
202
219
  shard
203
220
  )
221
+ # TODO: support async mode for sharded dry-runs
204
222
  sharded_integration.run(True)
205
223
 
206
224
  # run all shards
@@ -220,8 +238,11 @@ def _integration_dry_run[RunParamsTypeVar: RunParams](
220
238
  else:
221
239
  return
222
240
 
223
- # if not, we run the integration in full
224
- integration.run(True)
241
+ if isinstance(integration, QontractReconcileIntegration):
242
+ # if not, we run the integration in full
243
+ integration.run(True)
244
+ else:
245
+ asyncio.run(integration.async_run(True))
225
246
 
226
247
 
227
248
  def _is_task_result_an_error(result: Any) -> bool: