data-platform-helpers 0.1.0__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.
|
File without changes
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Copyright 2024 Canonical Ltd.
|
|
2
|
+
# See LICENSE file for licensing details.
|
|
3
|
+
|
|
4
|
+
"""Class used to determine if a relevant version attribute across related applications are valid.
|
|
5
|
+
|
|
6
|
+
There are many potential applications for this. Here are a few examples:
|
|
7
|
+
1. in a sharded cluster where is is important that the shards and cluster manager have the same
|
|
8
|
+
components.
|
|
9
|
+
2. kafka connect and kafka broker apps working together and needing to have the same ubnderlying
|
|
10
|
+
version.
|
|
11
|
+
|
|
12
|
+
How to use:
|
|
13
|
+
|
|
14
|
+
1. in src/charm.py of requirer + provider
|
|
15
|
+
in constructor [REQUIRED]:
|
|
16
|
+
self.version_checker = self.CrossAppVersionChecker(
|
|
17
|
+
self,
|
|
18
|
+
version=x, # can be a revision of a charm, version of a snap, version of a workload, etc
|
|
19
|
+
relations_to_check=[x,y,z],
|
|
20
|
+
# only use if the version doesn't not need to exactly match our current version
|
|
21
|
+
version_validity_range={"x": "<a,>b"})
|
|
22
|
+
|
|
23
|
+
in update status hook [OPTIONAL]:
|
|
24
|
+
if not self.version_checker.are_related_apps_valid():
|
|
25
|
+
logger.debug(
|
|
26
|
+
"Warning relational version check failed, these relations have mismatched versions",
|
|
27
|
+
"%s",
|
|
28
|
+
self.version_checker(self.version_checker.get_invalid_versions())
|
|
29
|
+
)
|
|
30
|
+
# can set status, instruct user to change
|
|
31
|
+
|
|
32
|
+
2. other areas of the charm (i.e. joined events, action events, etc) [OPTIONAL]:
|
|
33
|
+
if not self.charm.version_checker.are_related_apps_valid():
|
|
34
|
+
# do something - i.e. fail event or log message
|
|
35
|
+
|
|
36
|
+
3. in upgrade handler of requirer + provider [REQUIRED]:
|
|
37
|
+
if [last unit to upgrade]:
|
|
38
|
+
self.charm.version.set_version_across_all_relations()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
import logging
|
|
43
|
+
from typing import Dict, List, Optional, Tuple
|
|
44
|
+
|
|
45
|
+
from ops.charm import CharmBase
|
|
46
|
+
from ops.framework import Object
|
|
47
|
+
from ops.model import Unit
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
VERSION_CONST = "version"
|
|
52
|
+
DEPLOYMENT_TYPE = "deployment"
|
|
53
|
+
PREFIX_DIR = "/var/lib/juju/agents/"
|
|
54
|
+
LOCAL_BUILT_CHARM_PREFIX = "local"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_charm_revision(unit: Unit) -> int:
|
|
58
|
+
"""Returns the charm revision.
|
|
59
|
+
|
|
60
|
+
TODO: Keep this until ops framework supports: https://github.com/canonical/operator/issues/1255
|
|
61
|
+
"""
|
|
62
|
+
file_path = f"{PREFIX_DIR}unit-{unit.name.replace('/','-')}/charm/.juju-charm"
|
|
63
|
+
with open(file_path) as f:
|
|
64
|
+
charm_path = f.read().rstrip()
|
|
65
|
+
|
|
66
|
+
# revision of charm in a locally built chamr is unreliable:
|
|
67
|
+
# https://chat.canonical.com/canonical/pl/ro9935ayxbyyxn9hn6opy4f4xw
|
|
68
|
+
if charm_path.split(":")[0] == LOCAL_BUILT_CHARM_PREFIX:
|
|
69
|
+
logger.debug("Charm is locally built. Cannot determine revision number.")
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
# charm_path is of the format ch:amd64/jammy/<charm-name>-<revision number>
|
|
73
|
+
revision = charm_path.split("-")[-1]
|
|
74
|
+
return int(revision)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CrossAppVersionChecker(Object):
|
|
78
|
+
"""Verifies versions across multiple integrated applications."""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
charm: CharmBase,
|
|
83
|
+
version: int,
|
|
84
|
+
relations_to_check: List[str],
|
|
85
|
+
version_validity_range: Optional[Dict] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Constructor for CrossAppVersionChecker.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
charm: charm to inherit from
|
|
91
|
+
version: (int), the current version of the desired attribute of the charm
|
|
92
|
+
relations_to_check: (List), a list of relations who should have compatible versions
|
|
93
|
+
with the current charm
|
|
94
|
+
version_validity_range: (Optional Dict), a list of ranges for valid version ranges.
|
|
95
|
+
If not provided it is assumed that relations on the provided interface must have
|
|
96
|
+
the same version.
|
|
97
|
+
"""
|
|
98
|
+
super().__init__(charm, None)
|
|
99
|
+
self.charm = charm
|
|
100
|
+
# Future PR: upgrade this to a dictionary name versions
|
|
101
|
+
self.version = version
|
|
102
|
+
self.relations_to_check = relations_to_check
|
|
103
|
+
|
|
104
|
+
for rel in relations_to_check:
|
|
105
|
+
self.framework.observe(
|
|
106
|
+
charm.on[rel].relation_created,
|
|
107
|
+
self.set_version_on_relation_created,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# this feature has yet to be implemented, MongoDB does not need it and it is unclear if
|
|
111
|
+
# this will be extended to other charms. If this code is extended to other charms and
|
|
112
|
+
# there is a valid usecase we will use the `version_validity_range` variable in the
|
|
113
|
+
# function `get_invalid_versions`
|
|
114
|
+
self.version_validity_range = version_validity_range
|
|
115
|
+
|
|
116
|
+
def get_invalid_versions(self) -> List[Tuple[str, int]]:
|
|
117
|
+
"""Returns a list of (app name, version number) pairs, if the version number mismatches.
|
|
118
|
+
|
|
119
|
+
Mismatches are decided based on version_validity_range, if version_validity_range is not
|
|
120
|
+
provided, then the mismatch is expected to match this current app's version number.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
NoVersionError.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
invalid_relations = []
|
|
127
|
+
for relation_name in self.relations_to_check:
|
|
128
|
+
for relation in self.charm.model.relations[relation_name]:
|
|
129
|
+
related_version = relation.data[relation.app][VERSION_CONST]
|
|
130
|
+
if int(related_version) != self.version:
|
|
131
|
+
invalid_relations.append((relation.app.name, int(related_version)))
|
|
132
|
+
except KeyError:
|
|
133
|
+
raise NoVersionError(f"Expected {relation.app.name} to have version info.")
|
|
134
|
+
|
|
135
|
+
return invalid_relations
|
|
136
|
+
|
|
137
|
+
def get_version_of_related_app(self, related_app_name: str) -> int:
|
|
138
|
+
"""Returns a int for the version of the related app.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
NoVersionError.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
for relation_name in self.relations_to_check:
|
|
145
|
+
for rel in self.charm.model.relations[relation_name]:
|
|
146
|
+
if rel.app.name == related_app_name:
|
|
147
|
+
return int(rel.data[rel.app][VERSION_CONST])
|
|
148
|
+
except KeyError:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
raise NoVersionError(f"Expected {related_app_name} to have version info.")
|
|
152
|
+
|
|
153
|
+
def get_deployment_prefix(self) -> str:
|
|
154
|
+
"""Returns the deployment prefix, indicating if the charm is locally deployred or not.
|
|
155
|
+
|
|
156
|
+
TODO: Keep this until ops framework supports:
|
|
157
|
+
https://github.com/canonical/operator/issues/1255
|
|
158
|
+
"""
|
|
159
|
+
file_path = f"{PREFIX_DIR}/unit-{self.charm.unit.name.replace('/','-')}/charm/.juju-charm"
|
|
160
|
+
with open(file_path) as f:
|
|
161
|
+
charm_path = f.read().rstrip()
|
|
162
|
+
|
|
163
|
+
return charm_path.split(":")[0]
|
|
164
|
+
|
|
165
|
+
def are_related_apps_valid(self) -> bool:
|
|
166
|
+
"""Returns True if a related app has a version that's incompatible with the current app.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
NoVersionError.
|
|
170
|
+
"""
|
|
171
|
+
return self.get_invalid_versions() == []
|
|
172
|
+
|
|
173
|
+
def set_version_across_all_relations(self) -> None:
|
|
174
|
+
"""Sets the version number across all related apps, prvided by relations_to_check."""
|
|
175
|
+
if not self.charm.unit.is_leader():
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
for relation_name in self.relations_to_check:
|
|
179
|
+
for rel in self.charm.model.relations[relation_name]:
|
|
180
|
+
rel.data[self.charm.model.app][VERSION_CONST] = str(self.version)
|
|
181
|
+
rel.data[self.charm.model.app][DEPLOYMENT_TYPE] = str(self.get_deployment_prefix())
|
|
182
|
+
|
|
183
|
+
def set_version_on_related_app(self, relation_name: str, related_app_name: str) -> None:
|
|
184
|
+
"""Sets the version number across for a specified relation on a specified app."""
|
|
185
|
+
if not self.charm.unit.is_leader():
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
for rel in self.charm.model.relations[relation_name]:
|
|
189
|
+
if rel.app.name == related_app_name:
|
|
190
|
+
rel.data[self.charm.model.app][VERSION_CONST] = str(self.version)
|
|
191
|
+
rel.data[self.charm.model.app][DEPLOYMENT_TYPE] = str(self.get_deployment_prefix())
|
|
192
|
+
|
|
193
|
+
def set_version_on_relation_created(self, event) -> None:
|
|
194
|
+
"""Shares the charm's revision to the newly integrated application.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
RelationInvalidError
|
|
198
|
+
"""
|
|
199
|
+
if event.relation.name not in self.relations_to_check:
|
|
200
|
+
raise RelationInvalidError(
|
|
201
|
+
f"Provided relation: {event.relation.name} not in self.relations_to_check."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
self.set_version_on_related_app(event.relation.name, event.app.name)
|
|
205
|
+
|
|
206
|
+
def is_integrated_to_locally_built_charm(self) -> bool:
|
|
207
|
+
"""Returns a boolean value indicating whether the charm is integrated is a local charm.
|
|
208
|
+
|
|
209
|
+
NOTE: this function ONLY checks relations on the provided interfaces in
|
|
210
|
+
relations_to_check.
|
|
211
|
+
"""
|
|
212
|
+
for relation_name in self.relations_to_check:
|
|
213
|
+
for rel in self.charm.model.relations[relation_name]:
|
|
214
|
+
if rel.data[rel.app][DEPLOYMENT_TYPE] == LOCAL_BUILT_CHARM_PREFIX:
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
def is_local_charm(self, app_name: str) -> bool:
|
|
220
|
+
"""Returns a boolean value indicating whether the provided app is a local charm."""
|
|
221
|
+
if self.charm.app.name == app_name:
|
|
222
|
+
return self.get_deployment_prefix() == LOCAL_BUILT_CHARM_PREFIX
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
for relation_name in self.relations_to_check:
|
|
226
|
+
for rel in self.charm.model.relations[relation_name]:
|
|
227
|
+
if rel.app.name == app_name:
|
|
228
|
+
return rel.data[rel.app][DEPLOYMENT_TYPE] == LOCAL_BUILT_CHARM_PREFIX
|
|
229
|
+
except KeyError:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
raise NoVersionError(f"Expected {app_name} to have version info.")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class CrossAppVersionCheckerError(Exception):
|
|
236
|
+
"""Parent class for errors raised in CrossAppVersionChecker class."""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class RelationInvalidError(CrossAppVersionCheckerError):
|
|
240
|
+
"""Raised if a relation is not in the provided set of relations to check."""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class NoVersionError(CrossAppVersionCheckerError):
|
|
244
|
+
"""Raised if an application does not contain any version information."""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: data-platform-helpers
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Mia Altieri
|
|
6
|
+
Author-email: mgaltier200@gmail.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: ops (>=2.14.0,<3.0.0)
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
data_platform_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
data_platform_helpers/version_check.py,sha256=qKUBq_o6pn2Bi9a4ZylwQm2U0Ygr-S8kGLZpMn5NzBc,9815
|
|
3
|
+
data_platform_helpers-0.1.0.dist-info/METADATA,sha256=ldZUKhFu73Qudw1QarCRWvtCvnxsvOy1nhRSNUB-F_s,440
|
|
4
|
+
data_platform_helpers-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
5
|
+
data_platform_helpers-0.1.0.dist-info/RECORD,,
|