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
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  from collections.abc import Generator
2
3
  from typing import (
3
4
  Any,
@@ -11,6 +12,7 @@ from reconcile.utils.ocm.base import (
11
12
  OCMLabel,
12
13
  OCMOrganizationLabel,
13
14
  OCMSubscriptionLabel,
15
+ build_label_container,
14
16
  )
15
17
  from reconcile.utils.ocm.search_filters import Filter
16
18
  from reconcile.utils.ocm_base_client import OCMBaseClient
@@ -34,10 +36,25 @@ def add_subscription_label(
34
36
  ocm_cluster: OCMCluster,
35
37
  label: str,
36
38
  value: str,
39
+ ) -> None:
40
+ """Add the given label to the cluster subscription."""
41
+ add_label(
42
+ ocm_api=ocm_api,
43
+ label_container_href=f"{ocm_cluster.subscription.href}/labels",
44
+ label=label,
45
+ value=value,
46
+ )
47
+
48
+
49
+ def add_label(
50
+ ocm_api: OCMBaseClient,
51
+ label_container_href: str,
52
+ label: str,
53
+ value: str,
37
54
  ) -> None:
38
55
  """Add the given label to the cluster subscription."""
39
56
  ocm_api.post(
40
- api_path=f"{ocm_cluster.subscription.href}/labels",
57
+ api_path=label_container_href,
41
58
  data={"kind": "Label", "key": label, "value": value},
42
59
  )
43
60
 
@@ -54,11 +71,29 @@ def update_ocm_label(
54
71
  )
55
72
 
56
73
 
74
+ def update_label(
75
+ ocm_api: OCMBaseClient,
76
+ label_container_href: str,
77
+ label: str,
78
+ value: str,
79
+ ) -> None:
80
+ """Update the label value in the given OCM label."""
81
+ ocm_api.patch(
82
+ api_path=f"{label_container_href}/{label}",
83
+ data={"kind": "Label", "key": label, "value": value},
84
+ )
85
+
86
+
57
87
  def delete_ocm_label(ocm_api: OCMBaseClient, ocm_label: OCMLabel) -> None:
58
88
  """Delete the given OCM label."""
59
89
  ocm_api.delete(api_path=ocm_label.href)
60
90
 
61
91
 
92
+ def delete_label(ocm_api: OCMBaseClient, label_container_href: str, label: str) -> None:
93
+ """Delete the given OCM label."""
94
+ ocm_api.delete(api_path=f"{label_container_href}/{label}")
95
+
96
+
62
97
  def subscription_label_filter() -> Filter:
63
98
  """
64
99
  Returns a filter that can be used to find only subscription labels.
@@ -145,3 +180,28 @@ def label_filter(key: str, value: Optional[str] = None) -> Filter:
145
180
  if value:
146
181
  return lf.eq("value", value)
147
182
  return lf
183
+
184
+
185
+ def get_org_labels(
186
+ ocm_api: OCMBaseClient, org_ids: set[str], label_filter: Optional[Filter]
187
+ ) -> dict[str, LabelContainer]:
188
+ """
189
+ Fetch all labels from organizations. Optionally, label filtering can be
190
+ performed via the `label_filter` parameter.
191
+
192
+ The result is a dict with organization IDs as keys and label containers as values.
193
+ """
194
+ filter = Filter().is_in("organization_id", org_ids)
195
+ if label_filter:
196
+ filter &= label_filter
197
+ labels_by_org: dict[str, list[OCMOrganizationLabel]] = defaultdict(list)
198
+ for label in get_organization_labels(ocm_api, filter):
199
+ labels_by_org[label.organization_id].append(label)
200
+ return {
201
+ org_id: build_label_container(labels)
202
+ for org_id, labels in labels_by_org.items()
203
+ }
204
+
205
+
206
+ def build_organization_labels_href(org_id: str) -> str:
207
+ return f"/api/accounts_mgmt/v1/organizations/{org_id}/labels"
@@ -64,7 +64,7 @@ def main(
64
64
  glitchtip_project_query(query_func=gql.get_api().query).glitchtip_projects or []
65
65
  )
66
66
 
67
- apps = {project.app.path for project in glitchtip_projects}
67
+ apps = {project.app.path for project in glitchtip_projects if project.app}
68
68
 
69
69
  notification = Notification(
70
70
  notification_type="Action Required",
@@ -1,250 +0,0 @@
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 pydantic import (
14
- BaseModel,
15
- validator,
16
- )
17
-
18
- from reconcile.gql_definitions.fragments.ocm_environment import OCMEnvironment
19
- from reconcile.gql_definitions.ocm_subscription_labels.clusters import ClusterV1
20
- from reconcile.gql_definitions.ocm_subscription_labels.clusters import (
21
- query as cluster_query,
22
- )
23
- from reconcile.utils import gql
24
- from reconcile.utils.differ import diff_mappings
25
- from reconcile.utils.disabled_integrations import integration_is_enabled
26
- from reconcile.utils.helpers import flatten
27
- from reconcile.utils.ocm.clusters import (
28
- ClusterDetails,
29
- discover_clusters_for_organizations,
30
- )
31
- from reconcile.utils.ocm.labels import (
32
- add_subscription_label,
33
- delete_ocm_label,
34
- update_ocm_label,
35
- )
36
- from reconcile.utils.ocm_base_client import (
37
- OCMAPIClientConfigurationProtocol,
38
- OCMBaseClient,
39
- init_ocm_base_client,
40
- )
41
- from reconcile.utils.runtime.integration import (
42
- PydanticRunParams,
43
- QontractReconcileIntegration,
44
- )
45
- from reconcile.utils.secret_reader import SecretReaderBase
46
-
47
- QONTRACT_INTEGRATION = "ocm-subscription-labels"
48
-
49
-
50
- class EnvWithClusters(BaseModel):
51
- env: OCMEnvironment
52
- clusters: list[ClusterV1] = []
53
-
54
- class Config:
55
- arbitrary_types_allowed = True
56
-
57
-
58
- class ClusterLabelState(BaseModel):
59
- env: OCMEnvironment
60
- ocm_api: OCMBaseClient
61
- cluster_details: Optional[ClusterDetails] = None
62
- labels: dict[str, str] = {}
63
-
64
- class Config:
65
- arbitrary_types_allowed = True
66
-
67
- def __eq__(self, other: object) -> bool:
68
- if not isinstance(other, ClusterLabelState):
69
- raise NotImplementedError("Cannot compare to non ClusterState objects.")
70
- return self.labels == other.labels
71
-
72
-
73
- ClusterStates = dict[str, ClusterLabelState]
74
-
75
-
76
- class OcmLabelsIntegrationParams(PydanticRunParams):
77
- managed_label_prefixes: list[str] = []
78
-
79
- @validator("managed_label_prefixes")
80
- def must_end_with_dot( # pylint: disable=no-self-argument
81
- cls, v: list[str]
82
- ) -> list[str]:
83
- return [prefix + "." if not prefix.endswith(".") else prefix for prefix in v]
84
-
85
-
86
- class OcmLabelsIntegration(QontractReconcileIntegration[OcmLabelsIntegrationParams]):
87
- """Sync cluster.ocm-labels to OCM."""
88
-
89
- @property
90
- def name(self) -> str:
91
- return QONTRACT_INTEGRATION
92
-
93
- def get_clusters(self, query_func: Callable) -> list[ClusterV1]:
94
- data = cluster_query(query_func)
95
- return [
96
- c
97
- for c in data.clusters or []
98
- if c.ocm is not None and integration_is_enabled(self.name, c)
99
- ]
100
-
101
- def get_ocm_environments(
102
- self,
103
- clusters: Iterable[ClusterV1],
104
- ) -> list[EnvWithClusters]:
105
- envs: dict[str, EnvWithClusters] = {}
106
-
107
- for cluster in clusters:
108
- if cluster.ocm is None:
109
- # already filtered out in get_clusters - make mypy happy
110
- continue
111
- if cluster.ocm.environment.name not in envs:
112
- envs[cluster.ocm.environment.name] = EnvWithClusters(
113
- env=cluster.ocm.environment, clusters=[cluster]
114
- )
115
- else:
116
- envs[cluster.ocm.environment.name].clusters.append(cluster)
117
-
118
- return list(envs.values())
119
-
120
- def init_ocm_apis(
121
- self,
122
- envs: Iterable[EnvWithClusters],
123
- init_ocm_base_client: Callable[
124
- [OCMAPIClientConfigurationProtocol, SecretReaderBase], OCMBaseClient
125
- ] = init_ocm_base_client,
126
- ) -> None:
127
- self.ocm_apis = {
128
- env.env.name: init_ocm_base_client(env.env, self.secret_reader)
129
- for env in envs
130
- }
131
-
132
- def get_early_exit_desired_state(self) -> Optional[dict[str, Any]]:
133
- gqlapi = gql.get_api()
134
- return {"clusters": [c.dict() for c in self.get_clusters(gqlapi.query)]}
135
-
136
- def run(self, dry_run: bool) -> None:
137
- gqlapi = gql.get_api()
138
- clusters = self.get_clusters(gqlapi.query)
139
- current_state = self.fetch_current_state(
140
- clusters, self.params.managed_label_prefixes
141
- )
142
- desired_state = self.fetch_desired_state(clusters)
143
- self.reconcile(dry_run, current_state, desired_state)
144
-
145
- def fetch_current_state(
146
- self, clusters: Iterable[ClusterV1], managed_label_prefixes: list[str]
147
- ) -> ClusterStates:
148
- states: ClusterStates = {}
149
- envs = self.get_ocm_environments(clusters)
150
- self.init_ocm_apis(envs)
151
- for env in envs:
152
- for cluster_details in discover_clusters_for_organizations(
153
- ocm_api=self.ocm_apis[env.env.name],
154
- organization_ids=list({c.ocm.org_id for c in clusters if c.ocm}),
155
- ):
156
- filtered_labels = {
157
- label: value
158
- for label, value in cluster_details.subscription_labels.get_values_dict().items()
159
- if label.startswith(tuple(managed_label_prefixes))
160
- }
161
- states[cluster_details.ocm_cluster.name] = ClusterLabelState(
162
- env=env.env,
163
- ocm_api=self.ocm_apis[env.env.name],
164
- cluster_details=cluster_details,
165
- labels=filtered_labels,
166
- )
167
- return states
168
-
169
- def fetch_desired_state(self, clusters: Iterable[ClusterV1]) -> ClusterStates:
170
- states: ClusterStates = {}
171
- for cluster in clusters:
172
- if cluster.ocm is None:
173
- # already filtered out in get_clusters - make mypy happy
174
- continue
175
- states[cluster.name] = ClusterLabelState(
176
- env=cluster.ocm.environment,
177
- ocm_api=self.ocm_apis[cluster.ocm.environment.name],
178
- labels=flatten(cluster.ocm_subscription_labels or {}),
179
- )
180
-
181
- return states
182
-
183
- def reconcile(
184
- self,
185
- dry_run: bool,
186
- current_cluster_states: ClusterStates,
187
- desired_cluster_states: ClusterStates,
188
- ) -> None:
189
- for cluster_name, desired_cluster_state in desired_cluster_states.items():
190
- try:
191
- current_cluster_state = current_cluster_states[cluster_name]
192
- if not (cluster_details := current_cluster_state.cluster_details):
193
- # this should never happen - make mypy happy
194
- raise RuntimeError("Cluster details not found.")
195
-
196
- if desired_cluster_state == current_cluster_state:
197
- continue
198
- except KeyError:
199
- logging.info(
200
- f"Cluster '{cluster_name}' not found in OCM. Maybe it doesn't exist yet. Skipping."
201
- )
202
- continue
203
-
204
- diff_result = diff_mappings(
205
- current_cluster_state.labels, desired_cluster_state.labels
206
- )
207
-
208
- for label_to_add, value in diff_result.add.items():
209
- logging.info(
210
- [
211
- "create_cluster_subscription_label",
212
- cluster_name,
213
- f"{label_to_add}={value}",
214
- ]
215
- )
216
- if not dry_run:
217
- add_subscription_label(
218
- ocm_api=desired_cluster_state.ocm_api,
219
- ocm_cluster=cluster_details.ocm_cluster,
220
- label=label_to_add,
221
- value=value,
222
- )
223
- for label_to_rm, value in diff_result.delete.items():
224
- logging.info(
225
- [
226
- "delete_cluster_subscription_label",
227
- cluster_name,
228
- f"{label_to_rm}={value}",
229
- ]
230
- )
231
- if not dry_run:
232
- delete_ocm_label(
233
- ocm_api=desired_cluster_state.ocm_api,
234
- ocm_label=cluster_details.labels[label_to_rm],
235
- )
236
- for label_to_update, diff_pair in diff_result.change.items():
237
- value = diff_pair.desired
238
- logging.info(
239
- [
240
- "update_cluster_subscription_label",
241
- cluster_name,
242
- f"{label_to_update}={value}",
243
- ]
244
- )
245
- if not dry_run:
246
- update_ocm_label(
247
- ocm_api=desired_cluster_state.ocm_api,
248
- ocm_label=cluster_details.labels[label_to_update],
249
- value=value,
250
- )