qontract-reconcile 0.10.2.dev90__py3-none-any.whl → 0.10.2.dev92__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.dev90
3
+ Version: 0.10.2.dev92
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
@@ -208,11 +208,11 @@ reconcile/external_resources/secrets_sync.py,sha256=ZDxzGZ6wC4zxLhA7-L39xDRH6rzU
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
210
  reconcile/fleet_labeler/dependencies.py,sha256=ZOpmtwgxEPBWU2yHRc6rhPwlvqhwYSsNMJQ_jVq1dLI,2993
211
- reconcile/fleet_labeler/integration.py,sha256=GR-LUDfDp29VNy1aSLun1UwEOQ1AAXXC18ZdngdCsRg,8976
211
+ reconcile/fleet_labeler/integration.py,sha256=IeDUDlIIZREQRbsODB9xsCMoNrMk5738OF_fP7lUGFs,13128
212
212
  reconcile/fleet_labeler/merge_request.py,sha256=SfGxXInxeJzVnsTtO0ZC9-PesUJMdpKxKY9eCB6ms-g,1538
213
- reconcile/fleet_labeler/meta.py,sha256=DF7O4T9wvQ7-xTWXvuNw1OG_F0SBmRrjFBtVy9wWh9U,146
213
+ reconcile/fleet_labeler/meta.py,sha256=lWnpH2U0PHCPXu9Ok_CPmO494qQJQ5pOuqo28s0jzIQ,146
214
214
  reconcile/fleet_labeler/metrics.py,sha256=wx9BmXLsN67m-aSsf81iB7Ehj5SzUsS2WB75isUReZg,662
215
- reconcile/fleet_labeler/ocm.py,sha256=25UVRQDcbcJFQgg4WDXvUI5M8w9yqr0noiy90-sbQjY,2699
215
+ reconcile/fleet_labeler/ocm.py,sha256=qcg1_p7nKlZG7-MQeOZos3rz6YSPAPh-HKxE3OVJwe0,4165
216
216
  reconcile/fleet_labeler/validate.py,sha256=gzc2tt7h9F60h7dcyJfEmsnjnfuux5Jtc_WzrIqr-5k,2541
217
217
  reconcile/fleet_labeler/vcs.py,sha256=6UHUQ08AGAHXF7629I6X-T_E1pvx96LxjS66EeOzve4,1108
218
218
  reconcile/glitchtip/README.md,sha256=rfXT6jNP9khJW65jL7I2PgoxvxgcGGuJF8NpbzufEQ4,4335
@@ -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.dev90.dist-info/METADATA,sha256=ITNNwFXdctUPEAooDO8HTV-cc3Z7izzfNugSWuoAFwU,24565
790
- qontract_reconcile-0.10.2.dev90.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
791
- qontract_reconcile-0.10.2.dev90.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
792
- qontract_reconcile-0.10.2.dev90.dist-info/RECORD,,
789
+ qontract_reconcile-0.10.2.dev92.dist-info/METADATA,sha256=jLJhRpJR82Z3TVBJRPKXiwDJF_rs9_Vz9L9aaajBipo,24565
790
+ qontract_reconcile-0.10.2.dev92.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
791
+ qontract_reconcile-0.10.2.dev92.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
792
+ qontract_reconcile-0.10.2.dev92.dist-info/RECORD,,
@@ -26,6 +26,7 @@ from reconcile.typed_queries.fleet_labels import get_fleet_label_specs
26
26
  from reconcile.utils import (
27
27
  metrics,
28
28
  )
29
+ from reconcile.utils.differ import diff_mappings
29
30
  from reconcile.utils.jinja2.utils import process_jinja2_template
30
31
  from reconcile.utils.ruamel import create_ruamel_instance
31
32
  from reconcile.utils.runtime.integration import (
@@ -34,6 +35,18 @@ from reconcile.utils.runtime.integration import (
34
35
  )
35
36
 
36
37
 
38
+ class ClusterData(BaseModel):
39
+ """
40
+ Helper structure for synching process
41
+ """
42
+
43
+ name: str
44
+ server_url: str
45
+ subscription_id: str
46
+ desired_label_default: FleetLabelDefaultV1
47
+ current_subscription_labels: dict[str, str]
48
+
49
+
37
50
  class FleetLabelerIntegration(QontractReconcileIntegration[NoParams]):
38
51
  def __init__(self) -> None:
39
52
  super().__init__(NoParams())
@@ -59,13 +72,61 @@ class FleetLabelerIntegration(QontractReconcileIntegration[NoParams]):
59
72
  def reconcile(self, dependencies: Dependencies) -> None:
60
73
  validate_label_specs(specs=dependencies.label_specs_by_name)
61
74
  for spec_name, ocm in dependencies.ocm_clients_by_label_spec_name.items():
75
+ spec = dependencies.label_specs_by_name[spec_name]
76
+ discovered_clusters = self._discover_desired_clusters(
77
+ spec=spec,
78
+ ocm=ocm,
79
+ )
80
+ all_desired_clusters = {
81
+ k: v[0] for k, v in discovered_clusters.items() if len(v) == 1
82
+ }
83
+ clusters_with_duplicate_matches = {
84
+ k: v for k, v in discovered_clusters.items() if len(v) > 1
85
+ }
62
86
  self._sync_cluster_inventory(
63
87
  ocm=ocm,
64
88
  spec=dependencies.label_specs_by_name[spec_name],
65
89
  vcs=dependencies.vcs,
90
+ all_desired_clusters=all_desired_clusters,
91
+ clusters_with_duplicate_matches=clusters_with_duplicate_matches,
92
+ dry_run=dependencies.dry_run,
93
+ )
94
+ self._sync_subscription_labels(
95
+ spec=spec,
96
+ desired_clusters=all_desired_clusters,
97
+ ocm=ocm,
66
98
  dry_run=dependencies.dry_run,
67
99
  )
68
100
 
101
+ def _discover_desired_clusters(
102
+ self, spec: FleetLabelsSpecV1, ocm: OCMClient
103
+ ) -> dict[str, list[ClusterData]]:
104
+ clusters: dict[str, list[ClusterData]] = defaultdict(list)
105
+ for label_default in spec.label_defaults:
106
+ match_subscription_labels = dict(label_default.match_subscription_labels)
107
+ for cluster in ocm.discover_clusters_by_labels(
108
+ labels=match_subscription_labels,
109
+ managed_prefix=spec.managed_subscription_label_prefix,
110
+ ):
111
+ # Note, due to the nature of how our label filtering works (see ocm.py), we
112
+ # also fetch clusters that do not match the filter label.
113
+ # Here, we filter the clusters on client side.
114
+ # TODO: move this into utils.ocm module
115
+ if (
116
+ match_subscription_labels.items()
117
+ <= cluster.subscription_labels.items()
118
+ ):
119
+ clusters[cluster.cluster_id].append(
120
+ ClusterData(
121
+ subscription_id=cluster.subscription_id,
122
+ desired_label_default=label_default,
123
+ name=cluster.name,
124
+ server_url=cluster.server_url,
125
+ current_subscription_labels=cluster.subscription_labels,
126
+ )
127
+ )
128
+ return clusters
129
+
69
130
  def _render_default_labels(
70
131
  self,
71
132
  template: FleetSubscriptionLabelTemplateV1,
@@ -115,63 +176,91 @@ class FleetLabelerIntegration(QontractReconcileIntegration[NoParams]):
115
176
  yml.dump(content, stream)
116
177
  return stream.getvalue()
117
178
 
179
+ def _sync_subscription_labels(
180
+ self,
181
+ spec: FleetLabelsSpecV1,
182
+ desired_clusters: dict[str, ClusterData],
183
+ ocm: OCMClient,
184
+ dry_run: bool,
185
+ ) -> None:
186
+ """
187
+ Synchronize subscription labels for clusters in the spec's inventory.
188
+ Note, that we only update labels for clusters which are also part of the
189
+ discovered desired state.
190
+ I.e., we only operate on clusters that are in both, current state and desired state.
191
+ That way we ensure we do not work on deleted clusters and still synch only on
192
+ whats written in the rendered spec yet.
193
+ """
194
+ for cluster in spec.clusters:
195
+ if not desired_clusters.get(cluster.cluster_id):
196
+ # The cluster is not part of the desired inventory (will be updated with MR soon)
197
+ continue
198
+ # Ensure we only handle labels for our managed prefix
199
+ current_subscription_labels = {
200
+ k: v
201
+ for k, v in desired_clusters[
202
+ cluster.cluster_id
203
+ ].current_subscription_labels.items()
204
+ if k.startswith(spec.managed_subscription_label_prefix)
205
+ }
206
+ desired_subscription_labels = {
207
+ f"{spec.managed_subscription_label_prefix}.{k}": v
208
+ for k, v in dict(cluster.subscription_labels).items()
209
+ }
210
+ diff = diff_mappings(
211
+ current=current_subscription_labels,
212
+ desired=desired_subscription_labels,
213
+ )
214
+ for key in diff.add:
215
+ value = desired_subscription_labels[key]
216
+ logging.info(
217
+ f"[{spec.name}] Adding label '{key}={value}' for cluster '{cluster.cluster_id}' in subscription '{cluster.subscription_id}'."
218
+ )
219
+ if not dry_run:
220
+ ocm.add_subscription_label(
221
+ subscription_id=cluster.subscription_id,
222
+ key=key,
223
+ value=value,
224
+ )
225
+ for key in diff.change:
226
+ value = desired_subscription_labels[key]
227
+ logging.info(
228
+ f"[{spec.name}] Updating label '{key}={value}' for cluster '{cluster.cluster_id}' in subscription '{cluster.subscription_id}'."
229
+ )
230
+ if not dry_run:
231
+ ocm.update_subscription_label(
232
+ subscription_id=cluster.subscription_id,
233
+ key=key,
234
+ value=value,
235
+ )
236
+ # Note, we dont want to enable removal for now - its too dangerous on a broad managed prefix
237
+ # However, if it is needed in the future, we could easily add it here.
238
+
118
239
  def _sync_cluster_inventory(
119
240
  self,
120
241
  ocm: OCMClient,
121
242
  spec: FleetLabelsSpecV1,
122
243
  vcs: VCS,
244
+ clusters_with_duplicate_matches: dict[str, list[ClusterData]],
245
+ all_desired_clusters: dict[str, ClusterData],
123
246
  dry_run: bool,
124
247
  ) -> None:
125
- class ClusterData(BaseModel):
126
- """
127
- Helper structure for synching process
128
- """
129
-
130
- name: str
131
- server_url: str
132
- subscription_id: str
133
- label_default: FleetLabelDefaultV1
134
-
135
248
  all_current_cluster_ids = {cluster.cluster_id for cluster in spec.clusters}
136
- clusters: dict[str, list[ClusterData]] = defaultdict(list)
137
- for label_default in spec.label_defaults:
138
- match_subscription_labels = dict(label_default.match_subscription_labels)
139
- for cluster in ocm.discover_clusters_by_labels(
140
- labels=match_subscription_labels
141
- ):
142
- # TODO: ideally we filter on server side - see TODO in ocm.py
143
- if (
144
- match_subscription_labels.items()
145
- <= cluster.subscription_labels.items()
146
- ):
147
- clusters[cluster.cluster_id].append(
148
- ClusterData(
149
- label_default=label_default,
150
- subscription_id=cluster.subscription_id,
151
- name=cluster.name,
152
- server_url=cluster.server_url,
153
- )
154
- )
155
-
156
- cluster_with_duplicate_matches = {
157
- k: v for k, v in clusters.items() if len(v) > 1
158
- }
159
- for cluster_id, matches in cluster_with_duplicate_matches.items():
249
+ for cluster_id, matches in clusters_with_duplicate_matches.items():
160
250
  label_matches = "\n".join(
161
- str(m.label_default.match_subscription_labels) for m in matches
251
+ str(m.desired_label_default.match_subscription_labels) for m in matches
162
252
  )
163
253
  logging.error(
164
- f"Spec '{spec.name}': Cluster ID {cluster_id} is matched multiple times by different label matchers:\n{label_matches}"
254
+ f"[{spec.name}] Cluster ID {cluster_id} is matched multiple times by different label matchers:\n{label_matches}"
165
255
  )
166
256
  metrics.set_gauge(
167
257
  FleetLabelerDuplicateClusterMatchesGauge(
168
258
  integration=self.name,
169
259
  ocm_name=spec.ocm.name,
170
260
  ),
171
- len(cluster_with_duplicate_matches),
261
+ len(clusters_with_duplicate_matches),
172
262
  )
173
263
 
174
- all_desired_clusters = {k: v[0] for k, v in clusters.items() if len(v) == 1}
175
264
  clusters_to_add = [
176
265
  YamlCluster(
177
266
  cluster_id=cluster_id,
@@ -179,7 +268,7 @@ class FleetLabelerIntegration(QontractReconcileIntegration[NoParams]):
179
268
  name=cluster_info.name,
180
269
  server_url=cluster_info.server_url,
181
270
  subscription_labels_content=self._render_default_labels(
182
- template=cluster_info.label_default.subscription_label_template,
271
+ template=cluster_info.desired_label_default.subscription_label_template,
183
272
  labels=ocm.get_cluster_labels(cluster_id=cluster_id),
184
273
  ),
185
274
  )
@@ -223,4 +312,5 @@ class FleetLabelerIntegration(QontractReconcileIntegration[NoParams]):
223
312
  desired_content = self._render_yaml_file(
224
313
  current_content, cluster_ids_to_delete, sorted_clusters_to_add
225
314
  )
226
- vcs.open_merge_request(path=f"data{spec.path}", content=desired_content)
315
+ if not dry_run:
316
+ vcs.open_merge_request(path=f"data{spec.path}", content=desired_content)
@@ -1,4 +1,4 @@
1
1
  from reconcile.utils.semver_helper import make_semver
2
2
 
3
3
  QONTRACT_INTEGRATION = "fleet-labeler"
4
- QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
4
+ QONTRACT_INTEGRATION_VERSION = make_semver(1, 0, 0)
@@ -10,7 +10,9 @@ from reconcile.utils.ocm.clusters import (
10
10
  discover_clusters_by_labels,
11
11
  )
12
12
  from reconcile.utils.ocm.labels import (
13
+ add_label,
13
14
  get_cluster_labels_for_cluster_id,
15
+ update_label,
14
16
  )
15
17
  from reconcile.utils.ocm.search_filters import Filter, FilterMode
16
18
  from reconcile.utils.ocm_base_client import (
@@ -24,9 +26,9 @@ Thin abstractions of reconcile.ocm module to reduce coupling.
24
26
 
25
27
  class Cluster(BaseModel):
26
28
  cluster_id: str
29
+ subscription_id: str
27
30
  server_url: str
28
31
  name: str
29
- subscription_id: str
30
32
  subscription_labels: dict[str, str]
31
33
 
32
34
  @staticmethod
@@ -69,19 +71,56 @@ class OCMClient:
69
71
  def __init__(self, ocm_client: OCMBaseClient):
70
72
  self._ocm_client = ocm_client
71
73
 
72
- def discover_clusters_by_labels(self, labels: Mapping[str, str]) -> list[Cluster]:
74
+ def discover_clusters_by_labels(
75
+ self, labels: Mapping[str, str], managed_prefix: str
76
+ ) -> list[Cluster]:
73
77
  label_filter = Filter(mode=FilterMode.AND).eq("type", "Subscription")
74
78
  for key in labels:
75
79
  label_filter = label_filter.eq("Key", key)
76
- # TODO: This throws 400 bad request
77
- # for k, v in labels.items():
78
- # label_filter = label_filter.eq(k, v)
80
+ desired_labels_filter = (
81
+ Filter(mode=FilterMode.AND)
82
+ .eq("type", "Subscription")
83
+ .like(
84
+ "key",
85
+ f"{managed_prefix}.%",
86
+ )
87
+ )
88
+ # Note, that discover_clusters_by_labels() only fetches labels that are matching the filter!
89
+ # However, to understand current state, we also want to know the labels under managed_prefix.
90
+ # I.e., we need a fetch condition such as:
91
+ # ("{managed_prefix}.%") OR ("{key1_filter}" AND "{key2_filter}" ....)
92
+ # The above returns also clusters that do not fit the match label.
93
+ # I.e., we must filter the result on client side.
94
+ # TODO: do this in utils.ocm module
95
+ desired_filter = label_filter | desired_labels_filter
79
96
  return [
80
97
  Cluster.from_cluster_details(cluster)
81
98
  for cluster in discover_clusters_by_labels(
82
- ocm_api=self._ocm_client, label_filter=label_filter
99
+ ocm_api=self._ocm_client, label_filter=desired_filter
83
100
  )
84
101
  ]
85
102
 
86
103
  def get_cluster_labels(self, cluster_id: str) -> dict[str, str]:
87
104
  return get_cluster_labels_for_cluster_id(self._ocm_client, cluster_id)
105
+
106
+ def add_subscription_label(
107
+ self, subscription_id: str, key: str, value: str
108
+ ) -> None:
109
+ # TODO: move href into a utils function
110
+ add_label(
111
+ ocm_api=self._ocm_client,
112
+ label_container_href=f"/api/accounts_mgmt/v1/subscriptions/{subscription_id}/labels",
113
+ label=key,
114
+ value=value,
115
+ )
116
+
117
+ def update_subscription_label(
118
+ self, subscription_id: str, key: str, value: str
119
+ ) -> None:
120
+ # TODO: move href into a utils function
121
+ update_label(
122
+ ocm_api=self._ocm_client,
123
+ label_container_href=f"/api/accounts_mgmt/v1/subscriptions/{subscription_id}/labels",
124
+ label=key,
125
+ value=value,
126
+ )