qontract-reconcile 0.10.1rc348__py3-none-any.whl → 0.10.1rc350__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.
Files changed (25) hide show
  1. {qontract_reconcile-0.10.1rc348.dist-info → qontract_reconcile-0.10.1rc350.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.1rc348.dist-info → qontract_reconcile-0.10.1rc350.dist-info}/RECORD +24 -20
  3. reconcile/aus/advanced_upgrade_service.py +50 -13
  4. reconcile/aus/aus_label_source.py +115 -0
  5. reconcile/cli.py +4 -4
  6. reconcile/glitchtip/integration.py +64 -8
  7. reconcile/glitchtip/reconciler.py +1 -3
  8. reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py +11 -1
  9. reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py +9 -1
  10. reconcile/gql_definitions/fragments/aus_organization.py +9 -11
  11. reconcile/gql_definitions/fragments/minimal_ocm_organization.py +29 -0
  12. reconcile/gql_definitions/glitchtip/glitchtip_project.py +11 -2
  13. reconcile/gql_definitions/{ocm_subscription_labels → ocm_labels}/clusters.py +8 -0
  14. reconcile/gql_definitions/ocm_labels/organizations.py +72 -0
  15. reconcile/ldap_groups/integration.py +2 -1
  16. reconcile/ocm_labels/integration.py +406 -0
  17. reconcile/ocm_labels/label_sources.py +76 -0
  18. reconcile/utils/ocm/labels.py +61 -1
  19. tools/glitchtip_access_revalidation.py +1 -1
  20. reconcile/ocm_subscription_labels/integration.py +0 -250
  21. {qontract_reconcile-0.10.1rc348.dist-info → qontract_reconcile-0.10.1rc350.dist-info}/WHEEL +0 -0
  22. {qontract_reconcile-0.10.1rc348.dist-info → qontract_reconcile-0.10.1rc350.dist-info}/entry_points.txt +0 -0
  23. {qontract_reconcile-0.10.1rc348.dist-info → qontract_reconcile-0.10.1rc350.dist-info}/top_level.txt +0 -0
  24. /reconcile/gql_definitions/{ocm_subscription_labels → ocm_labels}/__init__.py +0 -0
  25. /reconcile/{ocm_subscription_labels → ocm_labels}/__init__.py +0 -0
@@ -0,0 +1,29 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ class ConfiguredBaseModel(BaseModel):
22
+ class Config:
23
+ smart_union = True
24
+ extra = Extra.forbid
25
+
26
+
27
+ class MinimalOCMOrganization(ConfiguredBaseModel):
28
+ name: str = Field(..., alias="name")
29
+ org_id: str = Field(..., alias="orgId")
@@ -61,12 +61,16 @@ query Projects {
61
61
  org_username
62
62
  }
63
63
  }
64
+ ldapGroups
65
+ membersOrganizationRole
64
66
  }
65
67
  organization {
66
68
  name
67
69
  instance {
68
70
  name
69
71
  }
72
+ # for glitchtip access revalidation
73
+ owners
70
74
  }
71
75
  # for glitchtip-project-dsn
72
76
  namespaces {
@@ -95,7 +99,7 @@ query Projects {
95
99
  }
96
100
  }
97
101
  }
98
- # for gltichtip access revalidation
102
+ # for glitchtip access revalidation
99
103
  app {
100
104
  path
101
105
  }
@@ -134,6 +138,10 @@ class RoleV1(ConfiguredBaseModel):
134
138
  class GlitchtipTeamV1(ConfiguredBaseModel):
135
139
  name: str = Field(..., alias="name")
136
140
  roles: list[RoleV1] = Field(..., alias="roles")
141
+ ldap_groups: Optional[list[str]] = Field(..., alias="ldapGroups")
142
+ members_organization_role: Optional[str] = Field(
143
+ ..., alias="membersOrganizationRole"
144
+ )
137
145
 
138
146
 
139
147
  class GlitchtipInstanceV1(ConfiguredBaseModel):
@@ -143,6 +151,7 @@ class GlitchtipInstanceV1(ConfiguredBaseModel):
143
151
  class GlitchtipProjectsV1_GlitchtipOrganizationV1(ConfiguredBaseModel):
144
152
  name: str = Field(..., alias="name")
145
153
  instance: GlitchtipInstanceV1 = Field(..., alias="instance")
154
+ owners: Optional[list[str]] = Field(..., alias="owners")
146
155
 
147
156
 
148
157
  class ClusterSpecV1(ConfiguredBaseModel):
@@ -187,7 +196,7 @@ class GlitchtipProjectsV1(ConfiguredBaseModel):
187
196
  ..., alias="organization"
188
197
  )
189
198
  namespaces: list[NamespaceV1] = Field(..., alias="namespaces")
190
- app: AppV1 = Field(..., alias="app")
199
+ app: Optional[AppV1] = Field(..., alias="app")
191
200
 
192
201
 
193
202
  class ProjectsQueryData(ConfiguredBaseModel):
@@ -41,6 +41,9 @@ fragment VaultSecret on VaultSecret_v1 {
41
41
  query OcmSubscriptionLabel {
42
42
  clusters: clusters_v1 {
43
43
  name
44
+ spec {
45
+ id
46
+ }
44
47
  ocm {
45
48
  environment {
46
49
  ...OCMEnvironment
@@ -63,6 +66,10 @@ class ConfiguredBaseModel(BaseModel):
63
66
  extra = Extra.forbid
64
67
 
65
68
 
69
+ class ClusterSpecV1(ConfiguredBaseModel):
70
+ q_id: Optional[str] = Field(..., alias="id")
71
+
72
+
66
73
  class OpenShiftClusterManagerV1(ConfiguredBaseModel):
67
74
  environment: OCMEnvironment = Field(..., alias="environment")
68
75
  org_id: str = Field(..., alias="orgId")
@@ -74,6 +81,7 @@ class DisableClusterAutomationsV1(ConfiguredBaseModel):
74
81
 
75
82
  class ClusterV1(ConfiguredBaseModel):
76
83
  name: str = Field(..., alias="name")
84
+ spec: Optional[ClusterSpecV1] = Field(..., alias="spec")
77
85
  ocm: Optional[OpenShiftClusterManagerV1] = Field(..., alias="ocm")
78
86
  disable: Optional[DisableClusterAutomationsV1] = Field(..., alias="disable")
79
87
  ocm_subscription_labels: Optional[Json] = Field(..., alias="ocmSubscriptionLabels")
@@ -0,0 +1,72 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ DEFINITION = """
22
+ query OcmOrganizations {
23
+ organizations: ocm_instances_v1 {
24
+ orgId
25
+ name
26
+ environment {
27
+ name
28
+ }
29
+ }
30
+ }
31
+ """
32
+
33
+
34
+ class ConfiguredBaseModel(BaseModel):
35
+ class Config:
36
+ smart_union = True
37
+ extra = Extra.forbid
38
+
39
+
40
+ class OpenShiftClusterManagerEnvironmentV1(ConfiguredBaseModel):
41
+ name: str = Field(..., alias="name")
42
+
43
+
44
+ class OpenShiftClusterManagerV1(ConfiguredBaseModel):
45
+ org_id: str = Field(..., alias="orgId")
46
+ name: str = Field(..., alias="name")
47
+ environment: OpenShiftClusterManagerEnvironmentV1 = Field(..., alias="environment")
48
+
49
+
50
+ class OcmOrganizationsQueryData(ConfiguredBaseModel):
51
+ organizations: Optional[list[OpenShiftClusterManagerV1]] = Field(
52
+ ..., alias="organizations"
53
+ )
54
+
55
+
56
+ def query(query_func: Callable, **kwargs: Any) -> OcmOrganizationsQueryData:
57
+ """
58
+ This is a convenience function which queries and parses the data into
59
+ concrete types. It should be compatible with most GQL clients.
60
+ You do not have to use it to consume the generated data classes.
61
+ Alternatively, you can also mime and alternate the behavior
62
+ of this function in the caller.
63
+
64
+ Parameters:
65
+ query_func (Callable): Function which queries your GQL Server
66
+ kwargs: optional arguments that will be passed to the query function
67
+
68
+ Returns:
69
+ OcmOrganizationsQueryData: queried data parsed into generated classes
70
+ """
71
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
72
+ return OcmOrganizationsQueryData(**raw_data)
@@ -116,7 +116,8 @@ class LdapGroupsIntegration(QontractReconcileIntegration[LdapGroupsIntegrationPa
116
116
  if not dry_run:
117
117
  state_obj["managed_groups"] = sorted(self._managed_groups)
118
118
 
119
- def get_integration_settings(self, query_func: Callable) -> LdapGroupsSettingsV1:
119
+ @staticmethod
120
+ def get_integration_settings(query_func: Callable) -> LdapGroupsSettingsV1:
120
121
  data = settings_query(query_func)
121
122
  if not data.settings:
122
123
  raise AppInterfaceSettingsError("No app-interface settings found.")
@@ -0,0 +1,406 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import (
5
+ Callable,
6
+ Iterable,
7
+ )
8
+ from typing import (
9
+ Any,
10
+ Optional,
11
+ )
12
+
13
+ from deepdiff import DeepHash
14
+ from pydantic import validator
15
+
16
+ from reconcile.aus.aus_label_source import (
17
+ init_aus_cluster_label_source,
18
+ init_aus_org_label_source,
19
+ )
20
+ from reconcile.gql_definitions.common.ocm_environments import (
21
+ query as ocm_environment_query,
22
+ )
23
+ from reconcile.gql_definitions.fragments.ocm_environment import OCMEnvironment
24
+ from reconcile.gql_definitions.ocm_labels.clusters import ClusterV1
25
+ from reconcile.gql_definitions.ocm_labels.clusters import query as cluster_query
26
+ from reconcile.gql_definitions.ocm_labels.organizations import OpenShiftClusterManagerV1
27
+ from reconcile.gql_definitions.ocm_labels.organizations import (
28
+ query as organization_query,
29
+ )
30
+ from reconcile.ocm_labels.label_sources import (
31
+ ClusterRef,
32
+ LabelSource,
33
+ LabelState,
34
+ OrgRef,
35
+ )
36
+ from reconcile.utils import gql
37
+ from reconcile.utils.differ import diff_mappings
38
+ from reconcile.utils.disabled_integrations import integration_is_enabled
39
+ from reconcile.utils.helpers import flatten
40
+ from reconcile.utils.ocm.clusters import discover_clusters_for_organizations
41
+ from reconcile.utils.ocm.labels import (
42
+ add_label,
43
+ build_organization_labels_href,
44
+ delete_label,
45
+ get_org_labels,
46
+ update_label,
47
+ )
48
+ from reconcile.utils.ocm.search_filters import Filter
49
+ from reconcile.utils.ocm_base_client import (
50
+ OCMAPIClientConfigurationProtocol,
51
+ OCMBaseClient,
52
+ init_ocm_base_client,
53
+ )
54
+ from reconcile.utils.runtime.integration import (
55
+ PydanticRunParams,
56
+ QontractReconcileIntegration,
57
+ )
58
+ from reconcile.utils.secret_reader import SecretReaderBase
59
+
60
+ QONTRACT_INTEGRATION = "ocm-labels"
61
+
62
+
63
+ class OcmLabelsIntegrationParams(PydanticRunParams):
64
+ managed_label_prefixes: list[str] = []
65
+
66
+ @validator("managed_label_prefixes")
67
+ def must_end_with_dot( # pylint: disable=no-self-argument
68
+ cls, v: list[str]
69
+ ) -> list[str]:
70
+ return [prefix + "." if not prefix.endswith(".") else prefix for prefix in v]
71
+
72
+
73
+ class ManagedLabelConflictError(Exception):
74
+ pass
75
+
76
+
77
+ class OcmLabelsIntegration(QontractReconcileIntegration[OcmLabelsIntegrationParams]):
78
+ """Sync labels to subscription and organizations."""
79
+
80
+ @property
81
+ def name(self) -> str:
82
+ return QONTRACT_INTEGRATION
83
+
84
+ def get_clusters(self, query_func: Callable) -> list[ClusterV1]:
85
+ data = cluster_query(query_func)
86
+ return [
87
+ c
88
+ for c in data.clusters or []
89
+ if c.ocm is not None and integration_is_enabled(self.name, c)
90
+ ]
91
+
92
+ def get_organizations(
93
+ self, query_func: Callable
94
+ ) -> list[OpenShiftClusterManagerV1]:
95
+ return organization_query(query_func).organizations or []
96
+
97
+ def get_environments(self, query_func: Callable) -> list[OCMEnvironment]:
98
+ return ocm_environment_query(query_func).environments
99
+
100
+ def init_ocm_apis(
101
+ self,
102
+ environments: Iterable[OCMEnvironment],
103
+ init_ocm_base_client: Callable[
104
+ [OCMAPIClientConfigurationProtocol, SecretReaderBase], OCMBaseClient
105
+ ] = init_ocm_base_client,
106
+ ) -> dict[str, OCMBaseClient]:
107
+ return {
108
+ env.name: init_ocm_base_client(env, self.secret_reader)
109
+ for env in environments
110
+ }
111
+
112
+ def org_label_sources(self, query_func: Callable) -> list[LabelSource]:
113
+ return [
114
+ init_aus_org_label_source(query_func),
115
+ ]
116
+
117
+ def subscription_label_sources(
118
+ self, clusters: list[ClusterV1], query_func: Callable
119
+ ) -> list[LabelSource]:
120
+ return [
121
+ init_cluster_subscription_label_source(clusters),
122
+ init_aus_cluster_label_source(query_func),
123
+ ]
124
+
125
+ def get_early_exit_desired_state(self) -> Optional[dict[str, Any]]:
126
+ gqlapi = gql.get_api()
127
+ desired = {
128
+ "org_labels": self.fetch_desired_state(
129
+ self.org_label_sources(gqlapi.query)
130
+ ),
131
+ "subs_labels": self.fetch_desired_state(
132
+ self.subscription_label_sources(
133
+ self.get_clusters(gqlapi.query), gqlapi.query
134
+ )
135
+ ),
136
+ }
137
+ # to figure out wheter to run a PR check of to exit early, a hash value
138
+ # of the desired state is sufficient
139
+ return {"hash": DeepHash(desired).get(desired)}
140
+
141
+ def run(self, dry_run: bool) -> None:
142
+ gqlapi = gql.get_api()
143
+ self.get_early_exit_desired_state()
144
+ clusters = self.get_clusters(gqlapi.query)
145
+ organizations = self.get_organizations(gqlapi.query)
146
+ environments = self.get_environments(gqlapi.query)
147
+
148
+ self.ocm_apis = self.init_ocm_apis(environments, init_ocm_base_client)
149
+
150
+ # organization labels
151
+ orgs_current_state, orgs_desired_state = self.fetch_organization_label_states(
152
+ organizations, gqlapi.query
153
+ )
154
+ self.reconcile(
155
+ dry_run=dry_run,
156
+ scope="organization",
157
+ current_state=orgs_current_state,
158
+ desired_state=orgs_desired_state,
159
+ )
160
+
161
+ # subscription labels
162
+ subs_current_state, subs_desired_state = self.fetch_subscription_label_states(
163
+ clusters, gqlapi.query
164
+ )
165
+ self.reconcile(
166
+ dry_run=dry_run,
167
+ scope="cluster",
168
+ current_state=subs_current_state,
169
+ desired_state=subs_desired_state,
170
+ )
171
+
172
+ def fetch_organization_label_states(
173
+ self, organizations: Iterable[OpenShiftClusterManagerV1], query_func: Callable
174
+ ) -> tuple[LabelState, LabelState]:
175
+ """
176
+ Returns the current and desired state of the organizations labels for
177
+ the given organizations.
178
+
179
+ Please note that the current state might not contain all requested organizations,
180
+ e.g. if a organization can't be found in OCM.
181
+ """
182
+ current_state = self.fetch_organization_label_current_state(
183
+ organizations, self.params.managed_label_prefixes
184
+ )
185
+ desired_state = self.fetch_desired_state(self.org_label_sources(query_func))
186
+ return current_state, desired_state
187
+
188
+ def fetch_organization_label_current_state(
189
+ self,
190
+ organizations: Iterable[OpenShiftClusterManagerV1],
191
+ managed_label_prefixes: list[str],
192
+ ) -> LabelState:
193
+ """
194
+ Fetches the current state of organizations labels for the given organizations.
195
+ If an organization can't be found in OCM, the resulting dict will not contain a
196
+ state for it, not even an empty one.
197
+
198
+ Only labels with a prefix in managed_label_prefixes are returned. Not every label
199
+ on an organizations is this integrations business.
200
+ """
201
+ states: LabelState = {
202
+ OrgRef(
203
+ org_id=org.org_id,
204
+ ocm_env=org.environment.name,
205
+ label_container_href=build_organization_labels_href(org.org_id),
206
+ name=org.name,
207
+ ): {}
208
+ for org in organizations
209
+ }
210
+
211
+ # prepare search filters
212
+ managed_label_filter = Filter()
213
+ for prefix in managed_label_prefixes:
214
+ managed_label_filter |= Filter().like("key", f"{prefix}%")
215
+
216
+ for env_name, ocm_api in self.ocm_apis.items():
217
+ env_orgs = {
218
+ o.org_id: o for o in organizations if o.environment.name == env_name
219
+ }
220
+ if not env_orgs:
221
+ continue
222
+ labels_by_org_id = get_org_labels(
223
+ ocm_api=ocm_api,
224
+ org_ids=set(env_orgs.keys()),
225
+ label_filter=managed_label_filter,
226
+ )
227
+ for org_id, labels in labels_by_org_id.items():
228
+ states[
229
+ OrgRef(
230
+ org_id=org_id,
231
+ ocm_env=env_name,
232
+ label_container_href=build_organization_labels_href(org_id),
233
+ name=env_orgs[org_id].name,
234
+ )
235
+ ] = labels.get_values_dict()
236
+
237
+ return states
238
+
239
+ def fetch_subscription_label_states(
240
+ self, clusters: list[ClusterV1], query_func: Callable
241
+ ) -> tuple[LabelState, LabelState]:
242
+ """
243
+ Returns the current and desired state of the subscription labels for
244
+ the given clusters.
245
+
246
+ Please note that the current state might not contain all requested clusters,
247
+ e.g. if a cluster can't be found in OCM or is not considered ready yet.
248
+ """
249
+ current_state = self.fetch_subscription_label_current_state(
250
+ clusters, self.params.managed_label_prefixes
251
+ )
252
+ desired_state = self.fetch_desired_state(
253
+ self.subscription_label_sources(clusters, query_func)
254
+ )
255
+ return current_state, desired_state
256
+
257
+ def fetch_subscription_label_current_state(
258
+ self, clusters: Iterable[ClusterV1], managed_label_prefixes: list[str]
259
+ ) -> LabelState:
260
+ """
261
+ Fetches the current state of subscription labels for the given clusters.
262
+ If a cluster can't be found in OCM, the resulting dict will not contain a
263
+ state for it, not even an empty one.
264
+
265
+ Only labels with a prefix in managed_label_prefixes are returned. Not every label
266
+ on a subscription is this integrations business.
267
+ """
268
+ cluster_ids = {c.spec.q_id for c in clusters if c.spec and c.spec.q_id}
269
+ states: LabelState = {}
270
+ for env_name, ocm_api in self.ocm_apis.items():
271
+ for cluster_details in discover_clusters_for_organizations(
272
+ ocm_api=ocm_api,
273
+ organization_ids=list(
274
+ {
275
+ c.ocm.org_id
276
+ for c in clusters
277
+ if c.ocm and c.ocm.environment.name == env_name
278
+ }
279
+ ),
280
+ ):
281
+ if cluster_details.ocm_cluster.id not in cluster_ids:
282
+ # there might be more clusters in an organization than we care about
283
+ continue
284
+
285
+ filtered_labels = {
286
+ label: value
287
+ for label, value in cluster_details.subscription_labels.get_values_dict().items()
288
+ if label.startswith(tuple(managed_label_prefixes))
289
+ }
290
+ states[
291
+ ClusterRef(
292
+ cluster_id=cluster_details.ocm_cluster.id,
293
+ org_id=cluster_details.organization_id,
294
+ ocm_env=env_name,
295
+ name=cluster_details.ocm_cluster.name,
296
+ label_container_href=f"{cluster_details.ocm_cluster.subscription.href}/labels",
297
+ )
298
+ ] = filtered_labels
299
+ return states
300
+
301
+ def fetch_desired_state(self, sources: list[LabelSource]) -> LabelState:
302
+ states: LabelState = {}
303
+ for s in sources:
304
+ for owner_ref, labels in s.get_labels().items():
305
+ if owner_ref not in states:
306
+ states[owner_ref] = {}
307
+ for label, value in labels.items():
308
+ if label in states[owner_ref]:
309
+ raise ManagedLabelConflictError(
310
+ f"The label {label} on {owner_ref.identity_labels()} is already managed by another label source"
311
+ )
312
+ states[owner_ref][label] = value
313
+
314
+ return states
315
+
316
+ def reconcile(
317
+ self,
318
+ dry_run: bool,
319
+ scope: str,
320
+ current_state: LabelState,
321
+ desired_state: LabelState,
322
+ ) -> None:
323
+ # we iterate via the current state because it refers to the clusters we can act on
324
+ for label_owner_ref, current_labels in current_state.items():
325
+ ocm_api = self.ocm_apis[label_owner_ref.ocm_env]
326
+ desired_labels = desired_state.get(label_owner_ref, {})
327
+ if current_labels == desired_labels:
328
+ continue
329
+
330
+ diff_result = diff_mappings(current_labels, desired_labels)
331
+
332
+ for label_to_add, value in diff_result.add.items():
333
+ logging.info(
334
+ [
335
+ f"create_{scope}_label",
336
+ *label_owner_ref.identity_labels(),
337
+ f"{label_to_add}={value}",
338
+ ]
339
+ )
340
+ if not dry_run:
341
+ add_label(
342
+ ocm_api=ocm_api,
343
+ label_container_href=label_owner_ref.required_label_container_href(),
344
+ label=label_to_add,
345
+ value=value,
346
+ )
347
+ for label_to_rm, value in diff_result.delete.items():
348
+ logging.info(
349
+ [
350
+ f"delete_{scope}_label",
351
+ *label_owner_ref.identity_labels(),
352
+ f"{label_to_rm}={value}",
353
+ ]
354
+ )
355
+ if not dry_run:
356
+ delete_label(
357
+ ocm_api=ocm_api,
358
+ label_container_href=label_owner_ref.required_label_container_href(),
359
+ label=label_to_rm,
360
+ )
361
+ for label_to_update, diff_pair in diff_result.change.items():
362
+ value = diff_pair.desired
363
+ logging.info(
364
+ [
365
+ f"update_{scope}_label",
366
+ *label_owner_ref.identity_labels(),
367
+ f"{label_to_update}={value}",
368
+ ]
369
+ )
370
+ if not dry_run:
371
+ update_label(
372
+ ocm_api=ocm_api,
373
+ label_container_href=label_owner_ref.required_label_container_href(),
374
+ label=label_to_update,
375
+ value=value,
376
+ )
377
+
378
+
379
+ def init_cluster_subscription_label_source(
380
+ clusters: list[ClusterV1],
381
+ ) -> ClusterSubscriptionLabelSource:
382
+ return ClusterSubscriptionLabelSource(
383
+ clusters=[
384
+ c
385
+ for c in clusters or []
386
+ if c.ocm is not None and integration_is_enabled(QONTRACT_INTEGRATION, c)
387
+ ],
388
+ )
389
+
390
+
391
+ class ClusterSubscriptionLabelSource(LabelSource):
392
+ def __init__(self, clusters: Iterable[ClusterV1]) -> None:
393
+ self.clusters = clusters
394
+
395
+ def get_labels(self) -> LabelState:
396
+ return {
397
+ ClusterRef(
398
+ cluster_id=cluster.spec.q_id,
399
+ org_id=cluster.ocm.org_id,
400
+ ocm_env=cluster.ocm.environment.name,
401
+ name=cluster.name,
402
+ label_container_href=None,
403
+ ): flatten(cluster.ocm_subscription_labels or {})
404
+ for cluster in self.clusters
405
+ if cluster.ocm and cluster.spec and cluster.spec.q_id
406
+ }
@@ -0,0 +1,76 @@
1
+ from abc import (
2
+ ABC,
3
+ abstractmethod,
4
+ )
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class LabelOwnerRef(ABC):
11
+ ocm_env: str
12
+ label_container_href: Optional[str]
13
+
14
+ @abstractmethod
15
+ def identity_labels(self) -> list[str]:
16
+ pass
17
+
18
+ def required_label_container_href(self) -> str:
19
+ if self.label_container_href is None:
20
+ raise ValueError(
21
+ "label_container_href is missing - this method should probably not be called in this state"
22
+ )
23
+ return self.label_container_href
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class OrgRef(LabelOwnerRef):
28
+ org_id: str
29
+ name: str
30
+
31
+ def __eq__(self, other: object) -> bool:
32
+ return (
33
+ isinstance(other, OrgRef)
34
+ and self.org_id == other.org_id
35
+ and self.ocm_env == other.ocm_env
36
+ )
37
+
38
+ def __hash__(self) -> int:
39
+ return hash((self.org_id, self.ocm_env))
40
+
41
+ def identity_labels(self) -> list[str]:
42
+ return [f"org_id={self.org_id}", f"org_name={self.name}"]
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class ClusterRef(LabelOwnerRef):
47
+ cluster_id: str
48
+ org_id: str
49
+ name: str
50
+
51
+ def __eq__(self, other: object) -> bool:
52
+ return (
53
+ isinstance(other, ClusterRef)
54
+ and self.cluster_id == other.cluster_id
55
+ and self.org_id == other.org_id
56
+ and self.ocm_env == other.ocm_env
57
+ )
58
+
59
+ def __hash__(self) -> int:
60
+ return hash((self.cluster_id, self.org_id, self.ocm_env))
61
+
62
+ def identity_labels(self) -> list[str]:
63
+ return [
64
+ f"org_id={self.org_id}",
65
+ f"cluster_id={self.cluster_id}",
66
+ f"cluster_name={self.name}",
67
+ ]
68
+
69
+
70
+ LabelState = dict[LabelOwnerRef, dict[str, str]]
71
+
72
+
73
+ class LabelSource(ABC):
74
+ @abstractmethod
75
+ def get_labels(self) -> LabelState:
76
+ pass