qontract-reconcile 0.10.2.dev85__py3-none-any.whl → 0.10.2.dev87__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.4
2
2
  Name: qontract-reconcile
3
- Version: 0.10.2.dev85
3
+ Version: 0.10.2.dev87
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Project-URL: homepage, https://github.com/app-sre/qontract-reconcile
6
6
  Project-URL: repository, https://github.com/app-sre/qontract-reconcile
@@ -199,22 +199,22 @@ reconcile/external_resources/aws.py,sha256=wzN3GHxyqVa4Lqqg5HdogqNW2RM532t0ZiKaQ
199
199
  reconcile/external_resources/factories.py,sha256=C0QHT0soEv6z99-ELAAE19S5MaMHhV0t1fSiQn0Coc4,5970
200
200
  reconcile/external_resources/integration.py,sha256=JF38M7R0Z4ADUTx57TZqSZH9k_xpPlbAxQAcGyIISuM,6925
201
201
  reconcile/external_resources/integration_secrets_sync.py,sha256=dX09O3r6KURziUYYfiki10orNjOGVma-XojhVqd0ww4,1667
202
- reconcile/external_resources/manager.py,sha256=ZagwLn6YQ1XmgmMN3qpuDzQsQxa4VOYl-IQPZBwCDqM,17103
202
+ reconcile/external_resources/manager.py,sha256=mvFfcXPzvNqDWDgKTK8eiSe6C_FUvBtben3bSqrqSoc,18246
203
203
  reconcile/external_resources/meta.py,sha256=noaytFzmShpzLA_ebGh7wuP45mOfHIOnnoUxivjDa1I,672
204
204
  reconcile/external_resources/metrics.py,sha256=KiBjMUaN_z0cSkF_7Ar_a8RiuiwVqjyMcVdISlxhzXE,3898
205
- reconcile/external_resources/model.py,sha256=dxwiyI3J9xyLeue8_W9NJoap-CkKLMAoY0S0ml5-NbU,13450
205
+ reconcile/external_resources/model.py,sha256=HrSkmAjOqw0aT3CsZKXKMLyocM__8XO5qHb-dXcoagw,13747
206
206
  reconcile/external_resources/reconciler.py,sha256=-0trp1K-iUgOQn3mm1ZUSmfaReRrUT0eHzPkUhNPolQ,9583
207
207
  reconcile/external_resources/secrets_sync.py,sha256=ZDxzGZ6wC4zxLhA7-L39xDRH6rzUM285gytuzmRQdlw,16208
208
208
  reconcile/external_resources/state.py,sha256=gF3ACdl7YiUlbQ4uEGrD6i_Txxqr6mT9f8IFlTQ-8dY,13176
209
209
  reconcile/fleet_labeler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
210
- reconcile/fleet_labeler/dependencies.py,sha256=Ta-SLnrHRN4OBAmhE_mTk1P7y1X7AInIiQsIYaY6hY0,2910
211
- reconcile/fleet_labeler/integration.py,sha256=gDK97QvsJ5IEo1t1ci4yxTRM7VSDj4uP9l30zHi_rWo,7700
210
+ reconcile/fleet_labeler/dependencies.py,sha256=ZOpmtwgxEPBWU2yHRc6rhPwlvqhwYSsNMJQ_jVq1dLI,2993
211
+ reconcile/fleet_labeler/integration.py,sha256=jm2wvkxsxqGLsX_vE874crgce_YsDWyggL3Q3olMNJM,8753
212
212
  reconcile/fleet_labeler/merge_request.py,sha256=VA_XSIob4LWYb02dRUr9coXjIa8_0prObI1v5Gqi_mY,1513
213
213
  reconcile/fleet_labeler/meta.py,sha256=DF7O4T9wvQ7-xTWXvuNw1OG_F0SBmRrjFBtVy9wWh9U,146
214
214
  reconcile/fleet_labeler/metrics.py,sha256=wx9BmXLsN67m-aSsf81iB7Ehj5SzUsS2WB75isUReZg,662
215
215
  reconcile/fleet_labeler/ocm.py,sha256=GGsz-bq1g8BJVVMCfI2kSwZCyngbQoZ3i3k8fO608KA,2506
216
216
  reconcile/fleet_labeler/validate.py,sha256=gzc2tt7h9F60h7dcyJfEmsnjnfuux5Jtc_WzrIqr-5k,2541
217
- reconcile/fleet_labeler/vcs.py,sha256=v4e_3l8F6aquVfe-ItLv2WJtS0kjMiRZ6DQ4mzCLOlE,726
217
+ reconcile/fleet_labeler/vcs.py,sha256=6UHUQ08AGAHXF7629I6X-T_E1pvx96LxjS66EeOzve4,1108
218
218
  reconcile/glitchtip/README.md,sha256=rfXT6jNP9khJW65jL7I2PgoxvxgcGGuJF8NpbzufEQ4,4335
219
219
  reconcile/glitchtip/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
220
220
  reconcile/glitchtip/integration.py,sha256=vCyg8W4ZUGxjU8tB1Gkre_auSpzo83n05mmO8_-7al0,8263
@@ -786,7 +786,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
786
786
  tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
787
787
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
788
788
  tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
789
- qontract_reconcile-0.10.2.dev85.dist-info/METADATA,sha256=vrGwAn3HqOrv1hAUy75U95p-JGo8Qf35bTOhuopBq0U,24565
790
- qontract_reconcile-0.10.2.dev85.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
791
- qontract_reconcile-0.10.2.dev85.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
792
- qontract_reconcile-0.10.2.dev85.dist-info/RECORD,,
789
+ qontract_reconcile-0.10.2.dev87.dist-info/METADATA,sha256=nVRpZDkDyzcxrgE5yIHcfaAKhAQBbpzHtq_p3vStPr8,24565
790
+ qontract_reconcile-0.10.2.dev87.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
791
+ qontract_reconcile-0.10.2.dev87.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
792
+ qontract_reconcile-0.10.2.dev87.dist-info/RECORD,,
@@ -133,6 +133,40 @@ class ExternalResourcesManager:
133
133
  self.thread_pool_size = thread_pool_size
134
134
  self.dry_runs_validator = dry_runs_validator
135
135
 
136
+ def _resource_spec_changed(
137
+ self, reconciliation: Reconciliation, state: ExternalResourceState
138
+ ) -> bool:
139
+ return reconciliation.resource_hash != state.reconciliation.resource_hash
140
+
141
+ def _resource_drift_detection_ttl_expired(
142
+ self, reconciliation: Reconciliation, state: ExternalResourceState
143
+ ) -> bool:
144
+ return (datetime.now(state.ts.tzinfo) - state.ts).total_seconds() > (
145
+ reconciliation.module_configuration.reconcile_drift_interval_minutes * 60
146
+ )
147
+
148
+ def _reconciliation_module_config_overridden(
149
+ self, reconciliation: Reconciliation, state: ExternalResourceState
150
+ ) -> bool:
151
+ return reconciliation.module_configuration.overridden and (
152
+ reconciliation.module_configuration.image_version
153
+ != state.reconciliation.module_configuration.image_version
154
+ )
155
+
156
+ def _reconciliation_needs_dry_run_run(
157
+ self, reconciliation: Reconciliation, state: ExternalResourceState
158
+ ) -> bool:
159
+ return (
160
+ reconciliation.action == Action.APPLY
161
+ and (
162
+ self._resource_spec_changed(reconciliation, state)
163
+ or self._reconciliation_module_config_overridden(reconciliation, state)
164
+ )
165
+ ) or (
166
+ reconciliation.action == Action.DESTROY
167
+ and not state.resource_status.is_in_progress
168
+ )
169
+
136
170
  def _get_reconcile_action(
137
171
  self, reconciliation: Reconciliation, state: ExternalResourceState
138
172
  ) -> ReconcileAction:
@@ -145,17 +179,16 @@ class ExternalResourcesManager:
145
179
  case ResourceStatus.ERROR:
146
180
  return ReconcileAction.APPLY_ERROR
147
181
  case ResourceStatus.CREATED | ResourceStatus.PENDING_SECRET_SYNC:
148
- if (
149
- reconciliation.resource_hash
150
- != state.reconciliation.resource_hash
151
- ):
182
+ if self._resource_spec_changed(reconciliation, state):
152
183
  return ReconcileAction.APPLY_SPEC_CHANGED
153
- elif (
154
- (datetime.now(state.ts.tzinfo) - state.ts).total_seconds()
155
- > reconciliation.module_configuration.reconcile_drift_interval_minutes
156
- * 60
184
+ elif self._resource_drift_detection_ttl_expired(
185
+ reconciliation, state
157
186
  ):
158
187
  return ReconcileAction.APPLY_DRIFT_DETECTION
188
+ elif self._reconciliation_module_config_overridden(
189
+ reconciliation, state
190
+ ):
191
+ return ReconcileAction.APPLY_MODULE_CONFIG_OVERRIDDEN
159
192
  elif reconciliation.action == Action.DESTROY:
160
193
  match state.resource_status:
161
194
  case ResourceStatus.CREATED:
@@ -411,18 +444,13 @@ class ExternalResourcesManager:
411
444
  self.dry_runs_validator.validate()
412
445
  desired_r = self._get_desired_objects_reconciliations()
413
446
  deleted_r = self._get_deleted_objects_reconciliations()
414
- reconciliations = desired_r.union(deleted_r)
415
- triggered: set[Reconciliation] = set()
416
-
417
- for r in reconciliations:
418
- state = self.state_mgr.get_external_resource_state(key=r.key)
419
- if (
420
- r.action == Action.APPLY
421
- and state.reconciliation.resource_hash != r.resource_hash
422
- ) or (
423
- r.action == Action.DESTROY and not state.resource_status.is_in_progress
424
- ):
425
- triggered.add(r)
447
+ triggered = {
448
+ r
449
+ for r in desired_r.union(deleted_r)
450
+ if self._reconciliation_needs_dry_run_run(
451
+ r, self.state_mgr.get_external_resource_state(key=r.key)
452
+ )
453
+ }
426
454
 
427
455
  threaded.run(
428
456
  self.reconciler.reconcile_resource,
@@ -7,7 +7,7 @@ from collections.abc import ItemsView, Iterable, Iterator, MutableMapping
7
7
  from enum import StrEnum
8
8
  from typing import Any
9
9
 
10
- from pydantic import BaseModel
10
+ from pydantic import BaseModel, Field
11
11
 
12
12
  from reconcile.external_resources.meta import (
13
13
  FLAG_DELETE_RESOURCE,
@@ -263,6 +263,7 @@ class ExternalResourceModuleConfiguration(BaseModel, frozen=True):
263
263
  outputs_secret_image: str = ""
264
264
  outputs_secret_version: str = ""
265
265
  resources: Resources = Resources()
266
+ overridden: bool = Field(default=False, exclude=True)
266
267
 
267
268
  @property
268
269
  def image_version(self) -> str:
@@ -278,17 +279,19 @@ class ExternalResourceModuleConfiguration(BaseModel, frozen=True):
278
279
  spec: ExternalResourceSpec,
279
280
  settings: ExternalResourcesSettingsV1,
280
281
  ) -> "ExternalResourceModuleConfiguration":
281
- module_overrides = spec.metadata.get(
282
- "module_overrides"
283
- ) or ExternalResourcesModuleOverrides(
284
- module_type=None,
285
- image=None,
286
- version=None,
287
- reconcile_timeout_minutes=None,
288
- outputs_secret_image=None,
289
- outputs_secret_version=None,
290
- resources=None,
291
- )
282
+ module_overrides = spec.metadata.get("module_overrides")
283
+ overridden = module_overrides is not None
284
+
285
+ if module_overrides is None:
286
+ module_overrides = ExternalResourcesModuleOverrides(
287
+ module_type=None,
288
+ image=None,
289
+ version=None,
290
+ reconcile_timeout_minutes=None,
291
+ outputs_secret_image=None,
292
+ outputs_secret_version=None,
293
+ resources=None,
294
+ )
292
295
 
293
296
  return ExternalResourceModuleConfiguration(
294
297
  image=module_overrides.image or module.image,
@@ -307,6 +310,7 @@ class ExternalResourceModuleConfiguration(BaseModel, frozen=True):
307
310
  or module.resources
308
311
  or settings.module_default_resources
309
312
  ),
313
+ overridden=overridden,
310
314
  )
311
315
 
312
316
 
@@ -358,6 +362,7 @@ class ReconcileAction(StrEnum):
358
362
  APPLY_SPEC_CHANGED = "Resource spec has changed"
359
363
  APPLY_DRIFT_DETECTION = "Resource drift detection run"
360
364
  APPLY_USER_REQUESTED = "Resource reconciliation requested"
365
+ APPLY_MODULE_CONFIG_OVERRIDDEN = "Module configuration overridden"
361
366
  DESTROY_CREATED = "Resource no longer exists in the configuration"
362
367
  DESTROY_ERROR = "Resource status in ERROR state"
363
368
 
@@ -27,10 +27,12 @@ class Dependencies:
27
27
  label_specs_by_name: Mapping[str, FleetLabelsSpecV1],
28
28
  ocm_clients_by_label_spec_name: Mapping[str, OCMClient],
29
29
  vcs: VCS,
30
+ dry_run: bool,
30
31
  ):
31
32
  self.label_specs_by_name = label_specs_by_name
32
33
  self.ocm_clients_by_label_spec_name = ocm_clients_by_label_spec_name
33
34
  self.vcs = vcs
35
+ self.dry_run = dry_run
34
36
 
35
37
  @classmethod
36
38
  def create(
@@ -42,6 +44,7 @@ class Dependencies:
42
44
  label_specs_by_name=_label_specs(),
43
45
  ocm_clients_by_label_spec_name=_ocm_clients(secret_reader=secret_reader),
44
46
  vcs=_vcs(secret_reader=secret_reader, dry_run=dry_run),
47
+ dry_run=dry_run,
45
48
  )
46
49
 
47
50
 
@@ -16,7 +16,7 @@ from reconcile.fleet_labeler.meta import (
16
16
  from reconcile.fleet_labeler.metrics import FleetLabelerDuplicateClusterMatchesGauge
17
17
  from reconcile.fleet_labeler.ocm import OCMClient
18
18
  from reconcile.fleet_labeler.validate import validate_label_specs
19
- from reconcile.fleet_labeler.vcs import VCS
19
+ from reconcile.fleet_labeler.vcs import VCS, Gitlab404Error
20
20
  from reconcile.gql_definitions.fleet_labeler.fleet_labels import (
21
21
  FleetLabelDefaultV1,
22
22
  FleetLabelsSpecV1,
@@ -60,7 +60,10 @@ class FleetLabelerIntegration(QontractReconcileIntegration[NoParams]):
60
60
  validate_label_specs(specs=dependencies.label_specs_by_name)
61
61
  for spec_name, ocm in dependencies.ocm_clients_by_label_spec_name.items():
62
62
  self._sync_cluster_inventory(
63
- ocm, dependencies.label_specs_by_name[spec_name], dependencies.vcs
63
+ ocm=ocm,
64
+ spec=dependencies.label_specs_by_name[spec_name],
65
+ vcs=dependencies.vcs,
66
+ dry_run=dependencies.dry_run,
64
67
  )
65
68
 
66
69
  def _render_default_labels(
@@ -112,7 +115,11 @@ class FleetLabelerIntegration(QontractReconcileIntegration[NoParams]):
112
115
  return stream.getvalue()
113
116
 
114
117
  def _sync_cluster_inventory(
115
- self, ocm: OCMClient, spec: FleetLabelsSpecV1, vcs: VCS
118
+ self,
119
+ ocm: OCMClient,
120
+ spec: FleetLabelsSpecV1,
121
+ vcs: VCS,
122
+ dry_run: bool,
116
123
  ) -> None:
117
124
  class ClusterData(BaseModel):
118
125
  """
@@ -189,7 +196,23 @@ class FleetLabelerIntegration(QontractReconcileIntegration[NoParams]):
189
196
  f"[{spec.name}] Deleting cluster {cluster_id=} from inventory."
190
197
  )
191
198
 
192
- current_content = vcs.get_file_content_from_main(path=spec.path)
199
+ # When adding a new label spec file, then we dont have any existing content in main yet.
200
+ # This is a chicken-egg problem, but if we are in dry-run we can skip these steps on 404s.
201
+ # Note, that the diff is already printed above, so we can make a good decision if desired
202
+ # content fits.
203
+ # If the content exists in main, then it doesnt harm to also run the rendering procedure.
204
+ try:
205
+ current_content = vcs.get_file_content_from_main(path=spec.path)
206
+ except Gitlab404Error:
207
+ if dry_run:
208
+ logging.info(
209
+ f"The file data{spec.path} does not exist in main branch yet. This is likely because it is being created with this MR. We are skipping rendering steps."
210
+ )
211
+ return
212
+ # 404 must never happen on non-dry-run, as the file must have already passed
213
+ # MR check and must have been merged to main
214
+ raise
215
+
193
216
  # Lets make sure we are deterministic when adding new clusters
194
217
  # The overhead is neglectable and it makes testing easier
195
218
  sorted_clusters_to_add = sorted(clusters_to_add, key=lambda c: c.name)
@@ -1,7 +1,13 @@
1
+ from gitlab.exceptions import GitlabGetError
2
+
1
3
  from reconcile.fleet_labeler.merge_request import FleetLabelerUpdates
2
4
  from reconcile.utils.vcs import VCS as VCSBase
3
5
 
4
6
 
7
+ class Gitlab404Error(Exception):
8
+ pass
9
+
10
+
5
11
  class VCS:
6
12
  """
7
13
  Thin abstractions of reconcile.utils.vcs module to reduce coupling and simplify tests.
@@ -11,9 +17,16 @@ class VCS:
11
17
  self._vcs = vcs
12
18
 
13
19
  def get_file_content_from_main(self, path: str) -> str:
14
- return self._vcs.get_file_content_from_app_interface_ref(
15
- file_path=path, ref="main"
16
- )
20
+ try:
21
+ return self._vcs.get_file_content_from_app_interface_ref(
22
+ file_path=path, ref="main"
23
+ )
24
+ except GitlabGetError as e:
25
+ if e.response_code != 404:
26
+ raise
27
+ raise Gitlab404Error(
28
+ f"File at ${path} does not exist yet in main branch. Maybe it is just being created with this MR?"
29
+ ) from e
17
30
 
18
31
  def open_merge_request(self, path: str, content: str) -> None:
19
32
  mr = FleetLabelerUpdates(path=path, content=content)