qontract-reconcile 0.10.1rc718__py3-none-any.whl → 0.10.1rc720__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.1rc718
3
+ Version: 0.10.1rc720
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
@@ -9,7 +9,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
9
9
  reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
10
10
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
11
11
  reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
12
- reconcile/cli.py,sha256=Xci_L0O3nPUPK3HA1NhIhbrzcPLZkXT65fCaJA66Tvg,96354
12
+ reconcile/cli.py,sha256=HLfkpQXhNA9YDwNWJUYg8zjm03sCEUIVB6-Ly4FOh48,96572
13
13
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
14
14
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
15
15
  reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
@@ -416,7 +416,7 @@ reconcile/templates/jira-checkpoint-missinginfo.j2,sha256=c_Vvg-lEENsB3tgxm9B6Y9
416
416
  reconcile/templates/rosa-classic-cluster-creation.sh.j2,sha256=0UHfYtXRVJqP07VJQx456cRI6EbZNBgamtP_8nb4WPY,2353
417
417
  reconcile/templates/rosa-hcp-cluster-creation.sh.j2,sha256=O7Bf3WQIJhsZoEqaYA0wRktUO4yXXCb4BQkuvvp-C80,2385
418
418
  reconcile/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
419
- reconcile/templating/renderer.py,sha256=xXCzRuhkDOCPELRKzjkAARTgs4iOoNcdDN_hlKJxyjg,8516
419
+ reconcile/templating/renderer.py,sha256=wWsuCUtmC3vdUHP9ZYZgafbQxuAxPGpXiMYWuewPEyg,10156
420
420
  reconcile/templating/validator.py,sha256=QGH2VSk7sVBuojhk9quRAbj_XBykHN-KZ53DbEneUJs,4391
421
421
  reconcile/templating/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
422
422
  reconcile/templating/lib/merge_request_manager.py,sha256=El3ufdVjHP4EW-LztX7zPiI9syJBJ6ETGRmiM9K4Nqw,5112
@@ -595,7 +595,7 @@ reconcile/utils/extended_early_exit.py,sha256=QSktrmfw37zSRMNk930tDbQsVeKxaPPPD4
595
595
  reconcile/utils/external_resource_spec.py,sha256=IRY8MCsyWKzt-Qj_hXiFKgkCvZu6VVyj6IYtqEb05BA,6618
596
596
  reconcile/utils/external_resources.py,sha256=a2CkJ3KLociYBnc_9F2VWfZGWMhzDl6fDNhwo2U-MWU,7501
597
597
  reconcile/utils/filtering.py,sha256=zZnHH0u0SaTDyzuFXZ_mREURGLvjEqQIQy4z-7QBVlc,419
598
- reconcile/utils/git.py,sha256=Qad7mfPuS9s7eKODeWSewehwSGgJPCbQuLda1qg_6GA,1522
598
+ reconcile/utils/git.py,sha256=BdxXFgQ1XOZpS-4qb3qMsKTCFDG8MlE26rv1jAhvCkM,1560
599
599
  reconcile/utils/git_secrets.py,sha256=0wGNL5mvDtVPRuu3vEQgld1Am64gIDJHtmu1_ZKxMAI,1973
600
600
  reconcile/utils/github_api.py,sha256=_bttNxYKeam_tLVe27L7O4gKqSn6CeyuFnJn8tSaUVY,2488
601
601
  reconcile/utils/gitlab_api.py,sha256=UMqWIHR_GkxEn5lgvxHLrjcwprRvRuKowxkwvu1xvvM,27241
@@ -764,8 +764,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
764
764
  tools/test/test_qontract_cli.py,sha256=UEwAW7PA_GIrbqzaLxpkCxbuVjEFLNvnVG-6VyoCGIc,4147
765
765
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
766
766
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
767
- qontract_reconcile-0.10.1rc718.dist-info/METADATA,sha256=QlMpHi8afUGz4ATxa8HHjMuCcJGKPjK9I3tFFFfCPXI,2382
768
- qontract_reconcile-0.10.1rc718.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
769
- qontract_reconcile-0.10.1rc718.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
770
- qontract_reconcile-0.10.1rc718.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
771
- qontract_reconcile-0.10.1rc718.dist-info/RECORD,,
767
+ qontract_reconcile-0.10.1rc720.dist-info/METADATA,sha256=37xORtZZIGEYo1VV4nfXuJQPGs88vCTYA0QSlRJ1WCY,2382
768
+ qontract_reconcile-0.10.1rc720.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
769
+ qontract_reconcile-0.10.1rc720.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
770
+ qontract_reconcile-0.10.1rc720.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
771
+ qontract_reconcile-0.10.1rc720.dist-info/RECORD,,
reconcile/cli.py CHANGED
@@ -1968,12 +1968,17 @@ def template_validator(ctx):
1968
1968
  @integration.command(short_help="Render datafile templates in app-interface.")
1969
1969
  @click.option(
1970
1970
  "--app-interface-data-path",
1971
- help="Path to app-interface dictory, used to write output and calculate diff in dry-run.",
1971
+ help="Path to data dir in app-interface repo. Use this for local rendering or in MR checks.",
1972
1972
  required=False,
1973
1973
  envvar="APP_INTERFACE_DATA_PATH",
1974
1974
  )
1975
+ @click.option(
1976
+ "--clone-repo",
1977
+ help="Path to a folder app-interface repo should be cloned to. Use this for regular integration run.",
1978
+ default=False,
1979
+ )
1975
1980
  @click.pass_context
1976
- def template_renderer(ctx, app_interface_data_path):
1981
+ def template_renderer(ctx, app_interface_data_path, clone_repo):
1977
1982
  from reconcile.templating.renderer import (
1978
1983
  TemplateRendererIntegration,
1979
1984
  TemplateRendererIntegrationParams,
@@ -1982,7 +1987,8 @@ def template_renderer(ctx, app_interface_data_path):
1982
1987
  run_class_integration(
1983
1988
  integration=TemplateRendererIntegration(
1984
1989
  TemplateRendererIntegrationParams(
1985
- app_interface_data_path=app_interface_data_path
1990
+ app_interface_data_path=app_interface_data_path,
1991
+ clone_repo=clone_repo,
1986
1992
  )
1987
1993
  ),
1988
1994
  ctx=ctx.obj,
@@ -2,12 +2,11 @@ import hashlib
2
2
  import json
3
3
  import logging
4
4
  import os
5
+ import tempfile
5
6
  from abc import ABC, abstractmethod
6
7
  from collections.abc import Callable
7
- from datetime import datetime, timedelta
8
- from typing import Any, Optional
8
+ from typing import Any, Optional, Self
9
9
 
10
- import gitlab
11
10
  from ruamel import yaml
12
11
 
13
12
  from reconcile.gql_definitions.templating.template_collection import (
@@ -29,20 +28,17 @@ from reconcile.typed_queries.app_interface_repo_url import get_app_interface_rep
29
28
  from reconcile.typed_queries.github_orgs import get_github_orgs
30
29
  from reconcile.typed_queries.gitlab_instances import get_gitlab_instances
31
30
  from reconcile.utils import gql
31
+ from reconcile.utils.git import clone
32
32
  from reconcile.utils.gql import init_from_config
33
33
  from reconcile.utils.ruamel import create_ruamel_instance
34
34
  from reconcile.utils.runtime.integration import (
35
35
  PydanticRunParams,
36
36
  QontractReconcileIntegration,
37
37
  )
38
- from reconcile.utils.state import State, init_state
39
38
  from reconcile.utils.vcs import VCS
40
39
 
41
40
  QONTRACT_INTEGRATION = "template-renderer"
42
41
 
43
- # Renders templates again to detect drift after one day
44
- CACHE_TTL_MINUTES = 24 * 60
45
-
46
42
  APP_INTERFACE_PATH_SEPERATOR = "/"
47
43
 
48
44
 
@@ -65,7 +61,13 @@ class FilePersistence(ABC):
65
61
 
66
62
 
67
63
  class LocalFilePersistence(FilePersistence):
64
+ """
65
+ This class provides a simple file persistence implementation for local files.
66
+ """
67
+
68
68
  def __init__(self, app_interface_data_path: str) -> None:
69
+ if not app_interface_data_path.endswith("/data"):
70
+ raise ValueError("app_interface_data_path should end with /data")
69
71
  self.app_interface_data_path = app_interface_data_path
70
72
 
71
73
  def write(self, outputs: list[TemplateOutput]) -> None:
@@ -90,8 +92,45 @@ class LocalFilePersistence(FilePersistence):
90
92
  return None
91
93
 
92
94
 
93
- class GitlabFilePersistence(FilePersistence):
94
- def __init__(self, vcs: VCS, mr_manager: MergeRequestManager) -> None:
95
+ class PersistenceTransaction(FilePersistence):
96
+ """
97
+ This class provides a context manager to make read/write operations
98
+ consistent. Reads/writes are beeing cached and writes are beeing delayed
99
+ until the context is left.
100
+ """
101
+
102
+ def __init__(self, persistence: FilePersistence, dry_run: bool) -> None:
103
+ self.persistence = persistence
104
+ self.dry_run = dry_run
105
+ self.content_cache: dict[str, Optional[str]] = {}
106
+ self.output_cache: dict[str, TemplateOutput] = {}
107
+
108
+ def write(self, outputs: list[TemplateOutput]) -> None:
109
+ for output in outputs:
110
+ self.content_cache[output.path] = output.content
111
+ self.output_cache[output.path] = output
112
+
113
+ def read(self, path: str) -> Optional[str]:
114
+ if path not in self.content_cache:
115
+ self.content_cache[path] = self.persistence.read(path)
116
+ return self.content_cache[path]
117
+
118
+ def __enter__(self) -> Self:
119
+ return self
120
+
121
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
122
+ if not self.dry_run:
123
+ self.persistence.write(list(self.output_cache.values()))
124
+
125
+
126
+ class ClonedRepoGitlabPersistence(FilePersistence):
127
+ """
128
+ This class is used to persist the rendered templates in a cloned gitlab repo
129
+ Reads are from the local filesystem, writes are done via utils.VCS abstraction
130
+ """
131
+
132
+ def __init__(self, local_path: str, vcs: VCS, mr_manager: MergeRequestManager):
133
+ self.local_path = join_path(local_path, "data")
95
134
  self.vcs = vcs
96
135
  self.mr_manager = mr_manager
97
136
 
@@ -101,10 +140,15 @@ class GitlabFilePersistence(FilePersistence):
101
140
 
102
141
  def read(self, path: str) -> Optional[str]:
103
142
  try:
104
- current = self.vcs.get_file_content_from_app_interface_master(path)
105
- except gitlab.exceptions.GitlabGetError:
106
- return None
107
- return current
143
+ with open(
144
+ f"{join_path(self.local_path, path)}",
145
+ "r",
146
+ encoding="utf-8",
147
+ ) as f:
148
+ return f.read()
149
+ except FileNotFoundError:
150
+ logging.debug(f"File not found: {path}, need to create it")
151
+ return None
108
152
 
109
153
 
110
154
  def unpack_static_variables(
@@ -125,6 +169,7 @@ def unpack_dynamic_variables(
125
169
 
126
170
 
127
171
  class TemplateRendererIntegrationParams(PydanticRunParams):
172
+ clone_repo: bool = False
128
173
  app_interface_data_path: Optional[str]
129
174
 
130
175
 
@@ -180,7 +225,6 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
180
225
  dry_run: bool,
181
226
  persistence: FilePersistence,
182
227
  ruamel_instance: yaml.YAML,
183
- state: Optional[State] = None,
184
228
  ) -> None:
185
229
  gql_no_validation = init_from_config(validate_schemas=False)
186
230
 
@@ -199,39 +243,23 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
199
243
  ).encode("utf-8")
200
244
  ).hexdigest()
201
245
 
202
- if state and state.exists(c.name):
203
- val = state.get(c.name)
204
- if val["hash"] == template_hash and datetime.fromisoformat(
205
- val["timestamp"]
206
- ) > (datetime.utcnow() - timedelta(minutes=CACHE_TTL_MINUTES)):
207
- logging.debug(f"Skipping {c.name} because it hasn't changed")
208
- break
209
-
210
- for template in c.templates:
211
- output = self.process_template(
212
- template,
213
- variables,
214
- persistence,
215
- ruamel_instance,
216
- )
217
- if output:
218
- output.input = TemplateInput(
219
- collection=c.name,
220
- collection_hash=template_hash,
221
- )
222
- outputs.append(output)
223
-
224
- if not dry_run:
225
- persistence.write(outputs)
226
- if state:
227
- state.add(
228
- c.name,
229
- {
230
- "hash": template_hash,
231
- "timestamp": datetime.utcnow().isoformat(),
232
- },
233
- force=True,
246
+ with PersistenceTransaction(persistence, dry_run) as p:
247
+ for template in c.templates:
248
+ output = self.process_template(
249
+ template,
250
+ variables,
251
+ p,
252
+ ruamel_instance,
234
253
  )
254
+ if output:
255
+ output.input = TemplateInput(
256
+ collection=c.name,
257
+ collection_hash=template_hash,
258
+ )
259
+ outputs.append(output)
260
+
261
+ if not dry_run:
262
+ p.write(outputs)
235
263
 
236
264
  @property
237
265
  def name(self) -> str:
@@ -239,14 +267,18 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
239
267
 
240
268
  def run(self, dry_run: bool) -> None:
241
269
  persistence: FilePersistence
242
- state: Optional[State] = None
243
- if self.params.app_interface_data_path:
270
+ ruaml_instance = create_ruamel_instance(explicit_start=True)
271
+
272
+ if not self.params.clone_repo and self.params.app_interface_data_path:
244
273
  persistence = LocalFilePersistence(self.params.app_interface_data_path)
245
- else:
274
+ self.reconcile(dry_run, persistence, ruaml_instance)
275
+
276
+ elif self.params.clone_repo:
277
+ gitlab_instances = get_gitlab_instances()
246
278
  vcs = VCS(
247
279
  secret_reader=self.secret_reader,
248
280
  github_orgs=get_github_orgs(),
249
- gitlab_instances=get_gitlab_instances(),
281
+ gitlab_instances=gitlab_instances,
250
282
  app_interface_repo_url=get_app_interface_repo_url(),
251
283
  dry_run=dry_run,
252
284
  allow_deleting_mrs=True,
@@ -256,10 +288,24 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
256
288
  vcs=vcs,
257
289
  parser=create_parser(),
258
290
  )
259
- persistence = GitlabFilePersistence(vcs, merge_request_manager)
291
+ url = get_app_interface_repo_url()
292
+
293
+ ssl_verify = next(
294
+ g.ssl_verify for g in gitlab_instances if url.startswith(g.url)
295
+ )
260
296
 
261
- state = init_state(QONTRACT_INTEGRATION, self.secret_reader)
297
+ with tempfile.TemporaryDirectory(
298
+ prefix=f"{QONTRACT_INTEGRATION}-",
299
+ ) as temp_dir:
300
+ logging.debug(f"Cloning {url} to {temp_dir}")
262
301
 
263
- ruaml_instance = create_ruamel_instance(explicit_start=True)
302
+ clone(url, temp_dir, depth=1, verify=ssl_verify)
264
303
 
265
- self.reconcile(dry_run, persistence, ruaml_instance, state=state)
304
+ persistence = ClonedRepoGitlabPersistence(
305
+ temp_dir, vcs, merge_request_manager
306
+ )
307
+
308
+ self.reconcile(dry_run, persistence, ruaml_instance)
309
+
310
+ else:
311
+ raise ValueError("App-interface-data-path must be set")
reconcile/utils/git.py CHANGED
@@ -6,9 +6,14 @@ class GitError(Exception):
6
6
  pass
7
7
 
8
8
 
9
- def clone(repo_url, wd):
10
- # pylint: disable=subprocess-run-check
11
- cmd = ["git", "clone", repo_url, wd]
9
+ def clone(repo_url, wd, depth=None, verify=True):
10
+ cmd = ["git"]
11
+ if not verify:
12
+ cmd += ["-c", "http.sslVerify=false"]
13
+ cmd += ["clone"]
14
+ if depth:
15
+ cmd += ["--depth", str(depth)]
16
+ cmd += [repo_url, wd]
12
17
  result = subprocess.run(
13
18
  cmd, cwd=wd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False
14
19
  )
@@ -17,7 +22,6 @@ def clone(repo_url, wd):
17
22
 
18
23
 
19
24
  def checkout(commit, wd):
20
- # pylint: disable=subprocess-run-check
21
25
  cmd = ["git", "checkout", commit]
22
26
  result = subprocess.run(
23
27
  cmd, cwd=wd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False
@@ -29,7 +33,6 @@ def checkout(commit, wd):
29
33
  def is_file_in_git_repo(file_path):
30
34
  real_path = os.path.realpath(file_path)
31
35
  dir_path = os.path.dirname(real_path)
32
- # pylint: disable=subprocess-run-check
33
36
  cmd = ["git", "rev-parse", "--is-inside-work-tree"]
34
37
  result = subprocess.run(
35
38
  cmd, cwd=dir_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False