qontract-reconcile 0.10.1rc950__py3-none-any.whl → 0.10.1rc952__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.1rc950
3
+ Version: 0.10.1rc952
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=q96mwr2KeEQ5bpNniGlgIMZTxiuLSodcYfX-t
10
10
  reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
11
11
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=O1wFp52EyF538c6txaWBs8eMtUIy19gyHZ6VzJ6QXS8,3512
12
12
  reconcile/checkpoint.py,sha256=_JhMxrye5BgkRMxWYuf7Upli6XayPINKSsuo3ynHTRc,5010
13
- reconcile/cli.py,sha256=rcy5rudCBXog_WSWGnHoTo8xZDeP-VHVXzF4sdpdrbU,105520
13
+ reconcile/cli.py,sha256=1uK79dvw3lF7p2oyOwK4552uywXqZ8MxRbv4WYvpoto,105531
14
14
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=rLh16BOlBOxTmJ8Si3wWyyEpmMlhh4Znx1Gc36qsmOc,4865
15
15
  reconcile/cluster_deployment_mapper.py,sha256=5gumAaRCcFXsabUJ1dnuUy9WrP_FEEM5JnOnE8ch9sE,2326
16
16
  reconcile/dashdotdb_base.py,sha256=l34QDu1G96_Ctnh7ZXdxXgSeCE93GQMdLAkWxmN6vDA,4775
@@ -463,12 +463,12 @@ reconcile/templates/jira-checkpoint-missinginfo.j2,sha256=c_Vvg-lEENsB3tgxm9B6Y9
463
463
  reconcile/templates/rosa-classic-cluster-creation.sh.j2,sha256=zqMMlKWV-aUDMtA-Xu5kl5pYLilewDClnKM0c807nNA,2146
464
464
  reconcile/templates/rosa-hcp-cluster-creation.sh.j2,sha256=hQ-WapiBNV3oEKQ813W3bD4HwQ4gADRPy80z9OLtW3Y,2317
465
465
  reconcile/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
466
- reconcile/templating/renderer.py,sha256=rcaGQ8sQjsAyWJKjsOFgKhLEUnPvJAh7eDAJBHHSQMs,12800
466
+ reconcile/templating/renderer.py,sha256=6qFeXNqAE8t85eewfm8Pcj9i_Fp2UJ1lad_zTqYmF7M,14753
467
467
  reconcile/templating/validator.py,sha256=5f9f35PCHOOdjb7KZquL2YdabyuAUokPDa4xutSEHIQ,5360
468
468
  reconcile/templating/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
469
469
  reconcile/templating/lib/merge_request_manager.py,sha256=XwpOR4rVS9ZiJ_Mn8qfCXPZ7CRLMGSJgeIkqD-8Jhgc,5228
470
470
  reconcile/templating/lib/model.py,sha256=YVUIXuPny3_kpFgBMSud8q_ndY5o882wKiX0l0A14L4,481
471
- reconcile/templating/lib/rendering.py,sha256=pbZv8uYlNtgzTjYMl5qSIW94vDS4CfE47aSjX4HpL-k,6210
471
+ reconcile/templating/lib/rendering.py,sha256=IzTbXJ5cO0c9mV6P9HrstOo79ovOcoNVtmyc6RgfMe0,6241
472
472
  reconcile/terraform_init/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
473
473
  reconcile/terraform_init/integration.py,sha256=glQ9uy8Kj2aTQXCAupwSFeih7reX_xMX_UuWW_ywBMU,6100
474
474
  reconcile/terraform_init/merge_request.py,sha256=3CYtgSd7Q9zjKg4wsDz437EPCRfGeZZ8fZ0Y-ChKXJY,1475
@@ -657,7 +657,7 @@ reconcile/utils/filtering.py,sha256=S4PbMHuFr3ED0P2Q_ea5CAaB7FimI62B-F5YTaKrphA,
657
657
  reconcile/utils/git.py,sha256=actOWI2HiNpMIV6nHCzinhRa6b04Y9plWOCcPQa8lNA,1437
658
658
  reconcile/utils/git_secrets.py,sha256=y1rEhwA8DyDpBSAEuhMS7Y2X3mpxT2zQ4zyDFkhLe_g,1936
659
659
  reconcile/utils/github_api.py,sha256=R8OvqyPdnRqvP-Efnv9RvIcbBlb4M0KC4RlbnJMD0Tg,2426
660
- reconcile/utils/gitlab_api.py,sha256=ZPU6e94DQ1T7pNO1VPoSq_iS0HfELN-gwYUyot42PdM,30334
660
+ reconcile/utils/gitlab_api.py,sha256=EUjo2IBmiz37tmmBmjadzyh6_Dh991G-QKD_NnftX6Q,30347
661
661
  reconcile/utils/gpg.py,sha256=EKG7_fdMv8BMlV5yUdPiqoTx-KrzmVSEAl2sLkaKwWI,1123
662
662
  reconcile/utils/gql.py,sha256=IGhxzBcuebbapDKLseevEThSsxa_eDCPNpo3A4VnOS4,14066
663
663
  reconcile/utils/grouping.py,sha256=vr9SFHZ7bqmHYrvYcEZt-Er3-yQYfAAdq5sHLZVmXPY,456
@@ -846,8 +846,8 @@ tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jr
846
846
  tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
847
847
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
848
848
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
849
- qontract_reconcile-0.10.1rc950.dist-info/METADATA,sha256=68rjYO_yguToaqfByY1BPGufGV1SFwkBdqZy5bt2OtU,2262
850
- qontract_reconcile-0.10.1rc950.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
851
- qontract_reconcile-0.10.1rc950.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
852
- qontract_reconcile-0.10.1rc950.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
853
- qontract_reconcile-0.10.1rc950.dist-info/RECORD,,
849
+ qontract_reconcile-0.10.1rc952.dist-info/METADATA,sha256=2-vhkaZkD_8sOM7nxeu08c69Q6nZCMcK-Qi2MBcUVRo,2262
850
+ qontract_reconcile-0.10.1rc952.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
851
+ qontract_reconcile-0.10.1rc952.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
852
+ qontract_reconcile-0.10.1rc952.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
853
+ qontract_reconcile-0.10.1rc952.dist-info/RECORD,,
reconcile/cli.py CHANGED
@@ -2084,7 +2084,8 @@ def template_validator(ctx):
2084
2084
  )
2085
2085
  @click.option(
2086
2086
  "--clone-repo",
2087
- help="Path to a folder app-interface repo should be cloned to. Use this for regular integration run.",
2087
+ is_flag=True,
2088
+ help="Flag to enable cloning of the app-interface repo. Use this for regular integration run.",
2088
2089
  default=False,
2089
2090
  )
2090
2091
  @click.option(
@@ -78,7 +78,7 @@ class Renderer(ABC):
78
78
  pass
79
79
 
80
80
  def render_target_path(self) -> str:
81
- return self._render_template(self.template.target_path)
81
+ return self._render_template(self.template.target_path).strip()
82
82
 
83
83
  def render_condition(self) -> bool:
84
84
  return self._render_template(self.template.condition or "True") == "True"
@@ -108,7 +108,7 @@ class PatchRenderer(Renderer):
108
108
  if self.template.patch is None: # here to satisfy mypy
109
109
  raise ValueError("PatchRenderer requires a patch")
110
110
 
111
- p = parse_jsonpath(self.template.patch.path)
111
+ p = parse_jsonpath(self._render_template(self.template.patch.path))
112
112
 
113
113
  matched_values = [match.value for match in p.find(self.data.current)]
114
114
 
@@ -58,10 +58,21 @@ def get_template_collections(
58
58
 
59
59
 
60
60
  class FilePersistence(ABC):
61
+ def __init__(self, dry_run: bool) -> None:
62
+ self.dry_run = dry_run
63
+ self.outputs: list[TemplateOutput] = []
64
+ self.result: TemplateResult | None = None
65
+
66
+ def __enter__(self) -> Self:
67
+ return self
68
+
61
69
  @abstractmethod
62
- def write(self, result: TemplateResult) -> None:
70
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
63
71
  pass
64
72
 
73
+ def write(self, output: TemplateOutput) -> None:
74
+ self.outputs.append(output)
75
+
65
76
  @abstractmethod
66
77
  def read(self, path: str) -> str | None:
67
78
  pass
@@ -84,40 +95,40 @@ class LocalFilePersistence(FilePersistence):
84
95
  This class provides a simple file persistence implementation for local files.
85
96
  """
86
97
 
87
- def __init__(self, app_interface_data_path: str) -> None:
98
+ def __init__(self, dry_run: bool, app_interface_data_path: str) -> None:
99
+ super().__init__(dry_run)
88
100
  if not app_interface_data_path.endswith("/data"):
89
101
  raise ValueError("app_interface_data_path should end with /data")
90
102
  self.app_interface_data_path = app_interface_data_path
91
103
 
92
- def write(self, result: TemplateResult) -> None:
93
- for output in result.outputs:
104
+ def read(self, path: str) -> str | None:
105
+ return self._read_local_file(join_path(self.app_interface_data_path, path))
106
+
107
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
108
+ if self.dry_run:
109
+ return
110
+ for output in self.outputs:
94
111
  filepath = Path(join_path(self.app_interface_data_path, output.path))
95
112
  filepath.parent.mkdir(parents=True, exist_ok=True)
96
113
  filepath.write_text(output.content, encoding="utf-8")
97
114
 
98
- def read(self, path: str) -> str | None:
99
- return self._read_local_file(join_path(self.app_interface_data_path, path))
100
-
101
115
 
102
116
  class PersistenceTransaction(FilePersistence):
103
117
  """
104
118
  This class provides a context manager to make read/write operations
105
- consistent. Reads/writes are beeing cached and writes are beeing delayed
119
+ consistent. Reads/writes are beeing cached and writes are being delayed
106
120
  until the context is left.
107
121
  """
108
122
 
109
- def __init__(self, persistence: FilePersistence, dry_run: bool) -> None:
123
+ def __init__(self, persistence: FilePersistence) -> None:
124
+ super().__init__(persistence.dry_run)
110
125
  self.persistence = persistence
111
- self.dry_run = dry_run
112
126
  self.content_cache: dict[str, str | None] = {}
113
127
  self.output_cache: dict[str, TemplateOutput] = {}
114
- self.result: TemplateResult
115
128
 
116
- def write(self, result: TemplateResult) -> None:
117
- self.result = result
118
- for output in result.outputs:
119
- self.content_cache[output.path] = output.content
120
- self.output_cache[output.path] = output
129
+ def write(self, output: TemplateOutput) -> None:
130
+ self.content_cache[output.path] = output.content
131
+ self.output_cache[output.path] = output
121
132
 
122
133
  def read(self, path: str) -> str | None:
123
134
  if path not in self.content_cache:
@@ -129,8 +140,8 @@ class PersistenceTransaction(FilePersistence):
129
140
 
130
141
  def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
131
142
  if not self.dry_run and self.output_cache:
132
- self.result.outputs = list(self.output_cache.values())
133
- self.persistence.write(self.result)
143
+ for output in self.output_cache.values():
144
+ self.persistence.write(output)
134
145
 
135
146
 
136
147
  class ClonedRepoGitlabPersistence(FilePersistence):
@@ -138,30 +149,64 @@ class ClonedRepoGitlabPersistence(FilePersistence):
138
149
  This class is used to persist the rendered templates in a cloned gitlab repo
139
150
  Reads are from the local filesystem, writes are done via utils.VCS abstraction
140
151
 
141
- Only one MR is created per run. Auto-approval MRs are prefered.
152
+ One MR is created per template-collection. Auto-approval MRs are prefered.
142
153
  """
143
154
 
144
- def __init__(self, local_path: str, vcs: VCS, mr_manager: MergeRequestManager):
155
+ def __init__(
156
+ self, dry_run: bool, local_path: str, vcs: VCS, mr_manager: MergeRequestManager
157
+ ):
158
+ super().__init__(dry_run)
145
159
  self.local_path = join_path(local_path, "data")
146
160
  self.vcs = vcs
147
161
  self.mr_manager = mr_manager
148
162
 
149
- def write(self, result: TemplateResult) -> None:
163
+ def read(self, path: str) -> str | None:
164
+ return self._read_local_file(join_path(self.local_path, path))
165
+
166
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
167
+ if self.result is None:
168
+ raise ValueError("ClonedRepoGitlabPersistence.result not set!")
169
+ self.result.outputs = self.outputs
170
+
171
+ if self.dry_run or not self.outputs:
172
+ return
173
+
150
174
  self.mr_manager.housekeeping()
151
175
 
152
- if result.enable_auto_approval:
153
- auto_approved = [o for o in result.outputs if o.auto_approved]
154
- if auto_approved:
155
- result.outputs = auto_approved
176
+ if self.result.enable_auto_approval:
177
+ if auto_approved_outputs := [o for o in self.outputs if o.auto_approved]:
178
+ # create an MR with auto-approved templates only
156
179
  self.mr_manager.create_merge_request(
157
- MrData(result=result, auto_approved=True)
180
+ MrData(
181
+ result=TemplateResult(
182
+ collection=f"{self.result.collection}-auto-approved",
183
+ enable_auto_approval=self.result.enable_auto_approval,
184
+ labels=self.result.labels,
185
+ outputs=auto_approved_outputs,
186
+ ),
187
+ auto_approved=True,
188
+ )
158
189
  )
159
- return
160
-
161
- self.mr_manager.create_merge_request(MrData(result=result, auto_approved=False))
162
-
163
- def read(self, path: str) -> str | None:
164
- return self._read_local_file(join_path(self.local_path, path))
190
+ if not_auto_approved_outputs := [
191
+ o for o in self.outputs if not o.auto_approved
192
+ ]:
193
+ # create an MR with not auto-approved templates only
194
+ self.mr_manager.create_merge_request(
195
+ MrData(
196
+ result=TemplateResult(
197
+ collection=f"{self.result.collection}-not-auto-approved",
198
+ enable_auto_approval=self.result.enable_auto_approval,
199
+ labels=self.result.labels,
200
+ outputs=not_auto_approved_outputs,
201
+ ),
202
+ auto_approved=False,
203
+ )
204
+ )
205
+ else:
206
+ # create an MR with all templates
207
+ self.mr_manager.create_merge_request(
208
+ MrData(result=self.result, auto_approved=False)
209
+ )
165
210
 
166
211
 
167
212
  def unpack_static_variables(
@@ -240,27 +285,24 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
240
285
  template, variables, secret_reader=self.secret_reader
241
286
  )
242
287
  target_path = r.render_target_path()
243
-
244
- current_str = persistence.read(
245
- target_path,
246
- )
247
- if current_str is None and template.patch:
288
+ current_file = persistence.read(target_path)
289
+ if current_file is None and template.patch:
248
290
  raise ValueError(f"Can not patch non-existing file {target_path}")
249
291
 
250
- if current_str:
251
- r.data.current = ruaml_instance.load(current_str)
292
+ if current_file:
293
+ r.data.current = ruaml_instance.load(current_file)
252
294
 
253
295
  if r.render_condition():
254
296
  output = r.render_output()
255
297
 
256
- if current_str != output:
298
+ if current_file != output:
257
299
  logging.info(
258
300
  f"diff for template {template.name} in target path {target_path}"
259
301
  )
260
302
  return TemplateOutput(
261
303
  path=target_path,
262
304
  content=output,
263
- is_new=current_str is None,
305
+ is_new=current_file is None,
264
306
  auto_approved=template.auto_approved or False,
265
307
  )
266
308
  return None
@@ -272,7 +314,7 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
272
314
  persistence: FilePersistence,
273
315
  ruamel_instance: yaml.YAML,
274
316
  each: dict[str, Any],
275
- ) -> list[TemplateOutput]:
317
+ ) -> None:
276
318
  variables = {}
277
319
  if collection.variables:
278
320
  variables = {
@@ -281,46 +323,39 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
281
323
  ),
282
324
  "static": unpack_static_variables(collection.variables, each),
283
325
  }
284
-
285
- outputs: list[TemplateOutput] = []
286
- for template in collection.templates:
287
- output = self.process_template(
288
- template, variables, persistence, ruamel_instance
289
- )
290
- if output:
291
- outputs.append(output)
292
-
293
- return outputs
326
+ with PersistenceTransaction(persistence) as persistence_transaction:
327
+ for template in collection.templates:
328
+ if output := self.process_template(
329
+ template, variables, persistence_transaction, ruamel_instance
330
+ ):
331
+ persistence_transaction.write(output)
294
332
 
295
333
  def reconcile(
296
- self,
297
- dry_run: bool,
298
- persistence: FilePersistence,
299
- ruamel_instance: yaml.YAML,
334
+ self, persistence: FilePersistence, ruamel_instance: yaml.YAML
300
335
  ) -> None:
301
- gql_no_validation = init_from_config(validate_schemas=False)
302
- for c in get_template_collections(name=self.params.template_collection_name):
336
+ gql_api = init_from_config(validate_schemas=False)
337
+ for collection in get_template_collections(
338
+ name=self.params.template_collection_name
339
+ ):
303
340
  for_each_items: list[dict[str, Any]] = [{}]
304
- if c.for_each and c.for_each.items:
305
- for_each_items = c.for_each.items
341
+ if collection.for_each and collection.for_each.items:
342
+ for_each_items = collection.for_each.items
306
343
  result = TemplateResult(
307
- collection=c.name,
308
- enable_auto_approval=c.enable_auto_approval or False,
309
- labels=c.additional_mr_labels or [],
344
+ collection=collection.name,
345
+ enable_auto_approval=collection.enable_auto_approval or False,
346
+ labels=collection.additional_mr_labels or [],
310
347
  )
311
- with PersistenceTransaction(persistence, dry_run) as p:
348
+ with persistence as p:
349
+ p.result = result
350
+ p.outputs = []
312
351
  for item in for_each_items:
313
- result.outputs.extend(
314
- self.reconcile_template_collection(
315
- c,
316
- gql_no_validation,
317
- p,
318
- ruamel_instance,
319
- item,
320
- )
352
+ self.reconcile_template_collection(
353
+ collection=collection,
354
+ gql_api=gql_api,
355
+ persistence=p,
356
+ ruamel_instance=ruamel_instance,
357
+ each=item,
321
358
  )
322
- if not dry_run and result.outputs:
323
- p.write(result)
324
359
 
325
360
  @property
326
361
  def name(self) -> str:
@@ -331,8 +366,11 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
331
366
  ruaml_instance = create_ruamel_instance(explicit_start=True)
332
367
 
333
368
  if not self.params.clone_repo and self.params.app_interface_data_path:
334
- persistence = LocalFilePersistence(self.params.app_interface_data_path)
335
- self.reconcile(dry_run, persistence, ruaml_instance)
369
+ persistence = LocalFilePersistence(
370
+ dry_run=dry_run,
371
+ app_interface_data_path=self.params.app_interface_data_path,
372
+ )
373
+ self.reconcile(persistence, ruaml_instance)
336
374
 
337
375
  elif self.params.clone_repo:
338
376
  gitlab_instances = get_gitlab_instances()
@@ -359,14 +397,14 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
359
397
  prefix=f"{QONTRACT_INTEGRATION}-",
360
398
  ) as temp_dir:
361
399
  logging.debug(f"Cloning {url} to {temp_dir}")
362
-
363
400
  clone(url, temp_dir, depth=1, verify=ssl_verify)
364
-
365
401
  persistence = ClonedRepoGitlabPersistence(
366
- temp_dir, vcs, merge_request_manager
402
+ dry_run=dry_run,
403
+ local_path=temp_dir,
404
+ vcs=vcs,
405
+ mr_manager=merge_request_manager,
367
406
  )
368
-
369
- self.reconcile(dry_run, persistence, ruaml_instance)
407
+ self.reconcile(persistence, ruaml_instance)
370
408
 
371
409
  else:
372
410
  raise ValueError("App-interface-data-path must be set")
@@ -130,7 +130,7 @@ class GitLabApi: # pylint: disable=too-many-public-methods
130
130
  @cached_property
131
131
  def project_main_branch(self) -> str:
132
132
  return next(
133
- (b.name for b in self.project.branches.list() if b.default),
133
+ (b.name for b in self.project.branches.list(iterator=True) if b.default),
134
134
  DEFAULT_MAIN_BRANCH,
135
135
  )
136
136