dpcharmlibs-interfaces 1.0.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.
- dpcharmlibs/interfaces/__init__.py +434 -0
- dpcharmlibs/interfaces/_version.py +15 -0
- dpcharmlibs/interfaces/diff.py +94 -0
- dpcharmlibs/interfaces/errors.py +34 -0
- dpcharmlibs/interfaces/events.py +333 -0
- dpcharmlibs/interfaces/handlers.py +1142 -0
- dpcharmlibs/interfaces/models.py +518 -0
- dpcharmlibs/interfaces/py.typed +0 -0
- dpcharmlibs/interfaces/repository.py +521 -0
- dpcharmlibs/interfaces/repository_interfaces.py +210 -0
- dpcharmlibs/interfaces/secrets.py +202 -0
- dpcharmlibs/interfaces/types.py +61 -0
- dpcharmlibs/interfaces/utils.py +67 -0
- dpcharmlibs_interfaces-1.0.0.dist-info/METADATA +266 -0
- dpcharmlibs_interfaces-1.0.0.dist-info/RECORD +16 -0
- dpcharmlibs_interfaces-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
r"""Library to manage the relation for the data-platform products.
|
|
16
|
+
|
|
17
|
+
This V1 has been specified in
|
|
18
|
+
https://docs.google.com/document/d/1lnuonWnoQb36RWYwfHOBwU0VClLbawpTISXIC_yNKYo,
|
|
19
|
+
and should be backward compatible with v0 clients.
|
|
20
|
+
|
|
21
|
+
This library contains the Requires and Provides classes for handling the relation
|
|
22
|
+
between an application and multiple managed application supported by the data-team:
|
|
23
|
+
MySQL, Postgresql, MongoDB, Redis, Kafka, and Karapace.
|
|
24
|
+
|
|
25
|
+
#### Models
|
|
26
|
+
|
|
27
|
+
This library exposes basic default models that can be used in most cases.
|
|
28
|
+
If you need more complex models, you can subclass them.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from dpcharmlibs.interfaces import RequirerCommonModel, ExtraSecretStr
|
|
32
|
+
|
|
33
|
+
class ExtendedCommonModel(RequirerCommonModel):
|
|
34
|
+
operator_password: ExtraSecretStr
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Secret groups are handled using annotated types.
|
|
38
|
+
If you wish to add extra secret groups, please follow the following model.
|
|
39
|
+
The string metadata represents the secret group name, and `OptionalSecretStr` is a TypeAlias for
|
|
40
|
+
`SecretStr | None`. Finally, `SecretStr` represents a field validating the URI pattern `secret:.*`
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
MyGroupSecretStr = Annotated[OptionalSecretStr, Field(exclude=True, default=None), "mygroup"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Fields not specified as OptionalSecretStr and extended with a group name in the metadata will NOT
|
|
47
|
+
get serialised.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
#### Requirer Charm
|
|
51
|
+
|
|
52
|
+
This library is a uniform interface to a selection of common database
|
|
53
|
+
metadata, with added custom events that add convenience to database management,
|
|
54
|
+
and methods to consume the application related data.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from dpcharmlibs.interfaces import (
|
|
59
|
+
RequirerCommonModel,
|
|
60
|
+
RequirerDataContractV1,
|
|
61
|
+
ResourceCreatedEvent,
|
|
62
|
+
ResourceEntityCreatedEvent,
|
|
63
|
+
ResourceProviderModel,
|
|
64
|
+
ResourceRequirerEventHandler,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
class ClientCharm(CharmBase):
|
|
68
|
+
# Database charm that accepts connections from application charms.
|
|
69
|
+
def __init__(self, *args) -> None:
|
|
70
|
+
super().__init__(*args)
|
|
71
|
+
|
|
72
|
+
requests = [
|
|
73
|
+
RequirerCommonModel(
|
|
74
|
+
resource="clientdb",
|
|
75
|
+
),
|
|
76
|
+
RequirerCommonModel(
|
|
77
|
+
resource="clientbis",
|
|
78
|
+
),
|
|
79
|
+
RequirerCommonModel(
|
|
80
|
+
entity_type="USER",
|
|
81
|
+
)
|
|
82
|
+
]
|
|
83
|
+
self.database = ResourceRequirerEventHandler(
|
|
84
|
+
self,"database", requests, response_model=ResourceProviderModel
|
|
85
|
+
)
|
|
86
|
+
self.framework.observe(self.database.on.resource_created, self._on_resource_created)
|
|
87
|
+
self.framework.observe(self.database.on.resource_entity_created, self._on_entity_created)
|
|
88
|
+
|
|
89
|
+
def _on_resource_created(self, event: ResourceCreatedEvent) -> None:
|
|
90
|
+
# Event triggered when a new database is created.
|
|
91
|
+
relation_id = event.relation.id
|
|
92
|
+
response = event.response # This is the response model
|
|
93
|
+
|
|
94
|
+
username = event.response.username
|
|
95
|
+
password = event.response.password
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
def _on_entity_created(self, event: ResourceCreatedEvent) -> None:
|
|
99
|
+
# Event triggered when a new entity is created.
|
|
100
|
+
...
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Compared to V0, this library makes heavy use of pydantic models, and allows for
|
|
104
|
+
multiple requests, specified as a list.
|
|
105
|
+
On the Requirer side, each response will trigger one custom event for that response.
|
|
106
|
+
This way, it allows for more strategic events to be emitted according to the request.
|
|
107
|
+
|
|
108
|
+
As show above, the library provides some custom events to handle specific situations,
|
|
109
|
+
which are listed below:
|
|
110
|
+
- resource_created: event emitted when the requested database is created.
|
|
111
|
+
- resource_entity_created: event emitted when the requested entity is created.
|
|
112
|
+
- endpoints_changed: event emitted when the read/write endpoints of the database have changed.
|
|
113
|
+
- read_only_endpoints_changed: event emitted when the read-only endpoints of the database
|
|
114
|
+
have changed. Event is not triggered if read/write endpoints changed too.
|
|
115
|
+
|
|
116
|
+
If it is needed to connect multiple database clusters to the same relation endpoint
|
|
117
|
+
the application charm can implement the same code as if it would connect to only
|
|
118
|
+
one database cluster (like the above code example).
|
|
119
|
+
|
|
120
|
+
To differentiate multiple clusters connected to the same relation endpoint
|
|
121
|
+
the application charm can use the name of the remote application:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
|
|
125
|
+
def _on_resource_created(self, event: ResourceCreatedEvent) -> None:
|
|
126
|
+
# Get the remote app name of the cluster that triggered this event
|
|
127
|
+
cluster = event.relation.app.name
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
It is also possible to provide an alias for each different database cluster/relation.
|
|
131
|
+
|
|
132
|
+
So, it is possible to differentiate the clusters in two ways.
|
|
133
|
+
The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
|
|
134
|
+
|
|
135
|
+
The second way is to use different event handlers to handle each cluster events.
|
|
136
|
+
The implementation would be something like the following code:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
|
|
140
|
+
from dpcharmlibs.interfaces import (
|
|
141
|
+
RequirerCommonModel,
|
|
142
|
+
RequirerDataContractV1,
|
|
143
|
+
ResourceCreatedEvent,
|
|
144
|
+
ResourceEntityCreatedEvent,
|
|
145
|
+
ResourceProviderModel,
|
|
146
|
+
ResourceRequirerEventHandler,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
class ApplicationCharm(CharmBase):
|
|
150
|
+
# Application charm that connects to database charms.
|
|
151
|
+
|
|
152
|
+
def __init__(self, *args):
|
|
153
|
+
super().__init__(*args)
|
|
154
|
+
|
|
155
|
+
requests = [
|
|
156
|
+
RequirerCommonModel(
|
|
157
|
+
resource="clientdb",
|
|
158
|
+
),
|
|
159
|
+
RequirerCommonModel(
|
|
160
|
+
resource="clientbis",
|
|
161
|
+
),
|
|
162
|
+
]
|
|
163
|
+
# Define the cluster aliases and one handler for each cluster database created event.
|
|
164
|
+
self.database = ResourceRequirerEventHandler(
|
|
165
|
+
self,
|
|
166
|
+
relation_name="database"
|
|
167
|
+
relations_aliases = ["cluster1", "cluster2"],
|
|
168
|
+
requests=
|
|
169
|
+
)
|
|
170
|
+
self.framework.observe(
|
|
171
|
+
self.database.on.cluster1_resource_created, self._on_cluster1_resource_created
|
|
172
|
+
)
|
|
173
|
+
self.framework.observe(
|
|
174
|
+
self.database.on.cluster2_resource_created, self._on_cluster2_resource_created
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _on_cluster1_resource_created(self, event: ResourceCreatedEvent) -> None:
|
|
178
|
+
# Handle the created database on the cluster named cluster1
|
|
179
|
+
|
|
180
|
+
# Create configuration file for app
|
|
181
|
+
config_file = self._render_app_config_file(
|
|
182
|
+
event.response.username,
|
|
183
|
+
event.response.password,
|
|
184
|
+
event.response.endpoints,
|
|
185
|
+
)
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
def _on_cluster2_resource_created(self, event: ResourceCreatedEvent) -> None:
|
|
189
|
+
# Handle the created database on the cluster named cluster2
|
|
190
|
+
|
|
191
|
+
# Create configuration file for app
|
|
192
|
+
config_file = self._render_app_config_file(
|
|
193
|
+
event.response.username,
|
|
194
|
+
event.response.password,
|
|
195
|
+
event.response.endpoints,
|
|
196
|
+
)
|
|
197
|
+
...
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Provider Charm
|
|
201
|
+
|
|
202
|
+
Following an example of using the ResourceRequestedEvent, in the context of the
|
|
203
|
+
database charm code:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from dpcharmlibs.interfaces import (
|
|
207
|
+
ResourceProviderEventHandler,
|
|
208
|
+
ResourceProviderModel,
|
|
209
|
+
ResourceRequestedEvent,
|
|
210
|
+
RequirerCommonModel,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
class SampleCharm(CharmBase):
|
|
214
|
+
|
|
215
|
+
def __init__(self, *args):
|
|
216
|
+
super().__init__(*args)
|
|
217
|
+
# Charm events defined in the database provides charm library.
|
|
218
|
+
self.provided_database = ResourceProviderEventHandler(self, "database", RequirerCommonModel)
|
|
219
|
+
self.framework.observe(self.provided_database.on.resource_requested,
|
|
220
|
+
self._on_resource_requested)
|
|
221
|
+
# Database generic helper
|
|
222
|
+
self.database = DatabaseHelper()
|
|
223
|
+
|
|
224
|
+
def _on_resource_requested(self, event: ResourceRequestedEvent) -> None:
|
|
225
|
+
# Handle the event triggered by a new database requested in the relation
|
|
226
|
+
# Retrieve the database name using the charm library.
|
|
227
|
+
db_name = event.request.resource
|
|
228
|
+
# generate a new user credential
|
|
229
|
+
username = self.database.generate_user(event.request.request_id)
|
|
230
|
+
password = self.database.generate_password(event.request.request_id)
|
|
231
|
+
# set the credentials for the relation
|
|
232
|
+
response = ResourceProviderModel(
|
|
233
|
+
salt=event.request.salt,
|
|
234
|
+
request_id=event.request.request_id,
|
|
235
|
+
resource=db_name,
|
|
236
|
+
username=username,
|
|
237
|
+
password=password,
|
|
238
|
+
...
|
|
239
|
+
)
|
|
240
|
+
self.provided_database.set_response(event.relation.id, response)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
As shown above, the library provides a custom event (resource_requested) to handle
|
|
244
|
+
the situation when an application charm requests a new database to be created.
|
|
245
|
+
It's preferred to subscribe to this event instead of relation changed event to avoid
|
|
246
|
+
creating a new database when other information other than a database name is
|
|
247
|
+
exchanged in the relation databag.
|
|
248
|
+
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
from ._version import __version__ as __version__
|
|
252
|
+
from .diff import (
|
|
253
|
+
Diff,
|
|
254
|
+
diff,
|
|
255
|
+
resource_added,
|
|
256
|
+
store_new_data,
|
|
257
|
+
)
|
|
258
|
+
from .errors import (
|
|
259
|
+
DataInterfacesError,
|
|
260
|
+
IllegalOperationError,
|
|
261
|
+
SecretAlreadyExistsError,
|
|
262
|
+
SecretError,
|
|
263
|
+
SecretsUnavailableError,
|
|
264
|
+
)
|
|
265
|
+
from .events import (
|
|
266
|
+
AuthenticationUpdatedEvent,
|
|
267
|
+
BulkResourcesRequestedEvent,
|
|
268
|
+
MtlsCertUpdatedEvent,
|
|
269
|
+
ResourceCreatedEvent,
|
|
270
|
+
ResourceEndpointsChangedEvent,
|
|
271
|
+
ResourceEntityCreatedEvent,
|
|
272
|
+
ResourceEntityPermissionsChangedEvent,
|
|
273
|
+
ResourceEntityRequestedEvent,
|
|
274
|
+
ResourceProviderEvent,
|
|
275
|
+
ResourceProvidesEvents,
|
|
276
|
+
ResourceReadOnlyEndpointsChangedEvent,
|
|
277
|
+
ResourceRequestedEvent,
|
|
278
|
+
ResourceRequirerEvent,
|
|
279
|
+
ResourceRequiresEvents,
|
|
280
|
+
StatusEventBase,
|
|
281
|
+
StatusRaisedEvent,
|
|
282
|
+
StatusResolvedEvent,
|
|
283
|
+
)
|
|
284
|
+
from .handlers import (
|
|
285
|
+
EventHandlers,
|
|
286
|
+
ResourceProviderEventHandler,
|
|
287
|
+
ResourceRequirerEventHandler,
|
|
288
|
+
)
|
|
289
|
+
from .models import (
|
|
290
|
+
SECRET_PREFIX,
|
|
291
|
+
BaseCommonModel,
|
|
292
|
+
CommonModel,
|
|
293
|
+
DataContract,
|
|
294
|
+
DataContractV0,
|
|
295
|
+
DataContractV1,
|
|
296
|
+
EntityPermissionModel,
|
|
297
|
+
KafkaRequestModel,
|
|
298
|
+
KafkaResponseModel,
|
|
299
|
+
PeerModel,
|
|
300
|
+
ProviderCommonModel,
|
|
301
|
+
RelationStatus,
|
|
302
|
+
RequirerCommonModel,
|
|
303
|
+
RequirerDataContract,
|
|
304
|
+
RequirerDataContractType,
|
|
305
|
+
RequirerDataContractV0,
|
|
306
|
+
RequirerDataContractV1,
|
|
307
|
+
ResourceProviderModel,
|
|
308
|
+
)
|
|
309
|
+
from .repository import (
|
|
310
|
+
AbstractRepository,
|
|
311
|
+
OpsOtherPeerUnitRepository,
|
|
312
|
+
OpsPeerRepository,
|
|
313
|
+
OpsPeerUnitRepository,
|
|
314
|
+
OpsRelationRepository,
|
|
315
|
+
OpsRepository,
|
|
316
|
+
)
|
|
317
|
+
from .repository_interfaces import (
|
|
318
|
+
OpsOtherPeerUnitRepositoryInterface,
|
|
319
|
+
OpsPeerRepositoryInterface,
|
|
320
|
+
OpsPeerUnitRepositoryInterface,
|
|
321
|
+
OpsRelationRepositoryInterface,
|
|
322
|
+
RepositoryInterface,
|
|
323
|
+
build_model,
|
|
324
|
+
write_model,
|
|
325
|
+
)
|
|
326
|
+
from .secrets import (
|
|
327
|
+
CachedSecret,
|
|
328
|
+
SecretCache,
|
|
329
|
+
)
|
|
330
|
+
from .types import (
|
|
331
|
+
EntitySecretStr,
|
|
332
|
+
ExtraSecretStr,
|
|
333
|
+
MtlsSecretStr,
|
|
334
|
+
OptionalPathLike,
|
|
335
|
+
OptionalSecretBool,
|
|
336
|
+
OptionalSecrets,
|
|
337
|
+
OptionalSecretStr,
|
|
338
|
+
RelationStatusDict,
|
|
339
|
+
Scope,
|
|
340
|
+
SecretGroup,
|
|
341
|
+
SecretString,
|
|
342
|
+
TlsSecretBool,
|
|
343
|
+
TlsSecretStr,
|
|
344
|
+
UserSecretStr,
|
|
345
|
+
)
|
|
346
|
+
from .utils import (
|
|
347
|
+
ensure_leader_for_app,
|
|
348
|
+
gen_hash,
|
|
349
|
+
gen_salt,
|
|
350
|
+
get_encoded_dict,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
__all__ = [
|
|
354
|
+
'SECRET_PREFIX',
|
|
355
|
+
'AbstractRepository',
|
|
356
|
+
'AuthenticationUpdatedEvent',
|
|
357
|
+
'BaseCommonModel',
|
|
358
|
+
'BulkResourcesRequestedEvent',
|
|
359
|
+
'CachedSecret',
|
|
360
|
+
'CommonModel',
|
|
361
|
+
'DataContract',
|
|
362
|
+
'DataContractV0',
|
|
363
|
+
'DataContractV1',
|
|
364
|
+
'DataInterfacesError',
|
|
365
|
+
'Diff',
|
|
366
|
+
'EntityPermissionModel',
|
|
367
|
+
'EntitySecretStr',
|
|
368
|
+
'EventHandlers',
|
|
369
|
+
'ExtraSecretStr',
|
|
370
|
+
'IllegalOperationError',
|
|
371
|
+
'KafkaRequestModel',
|
|
372
|
+
'KafkaResponseModel',
|
|
373
|
+
'MtlsCertUpdatedEvent',
|
|
374
|
+
'MtlsSecretStr',
|
|
375
|
+
'OpsOtherPeerUnitRepository',
|
|
376
|
+
'OpsOtherPeerUnitRepositoryInterface',
|
|
377
|
+
'OpsPeerRepository',
|
|
378
|
+
'OpsPeerRepositoryInterface',
|
|
379
|
+
'OpsPeerUnitRepository',
|
|
380
|
+
'OpsPeerUnitRepositoryInterface',
|
|
381
|
+
'OpsRelationRepository',
|
|
382
|
+
'OpsRelationRepositoryInterface',
|
|
383
|
+
'OpsRepository',
|
|
384
|
+
'OptionalPathLike',
|
|
385
|
+
'OptionalSecretBool',
|
|
386
|
+
'OptionalSecretStr',
|
|
387
|
+
'OptionalSecrets',
|
|
388
|
+
'PeerModel',
|
|
389
|
+
'ProviderCommonModel',
|
|
390
|
+
'RelationStatus',
|
|
391
|
+
'RelationStatusDict',
|
|
392
|
+
'RepositoryInterface',
|
|
393
|
+
'RequirerCommonModel',
|
|
394
|
+
'RequirerDataContract',
|
|
395
|
+
'RequirerDataContractType',
|
|
396
|
+
'RequirerDataContractV0',
|
|
397
|
+
'RequirerDataContractV1',
|
|
398
|
+
'ResourceCreatedEvent',
|
|
399
|
+
'ResourceEndpointsChangedEvent',
|
|
400
|
+
'ResourceEntityCreatedEvent',
|
|
401
|
+
'ResourceEntityPermissionsChangedEvent',
|
|
402
|
+
'ResourceEntityRequestedEvent',
|
|
403
|
+
'ResourceProviderEvent',
|
|
404
|
+
'ResourceProviderEventHandler',
|
|
405
|
+
'ResourceProviderModel',
|
|
406
|
+
'ResourceProvidesEvents',
|
|
407
|
+
'ResourceReadOnlyEndpointsChangedEvent',
|
|
408
|
+
'ResourceRequestedEvent',
|
|
409
|
+
'ResourceRequirerEvent',
|
|
410
|
+
'ResourceRequirerEventHandler',
|
|
411
|
+
'ResourceRequiresEvents',
|
|
412
|
+
'Scope',
|
|
413
|
+
'SecretAlreadyExistsError',
|
|
414
|
+
'SecretCache',
|
|
415
|
+
'SecretError',
|
|
416
|
+
'SecretGroup',
|
|
417
|
+
'SecretString',
|
|
418
|
+
'SecretsUnavailableError',
|
|
419
|
+
'StatusEventBase',
|
|
420
|
+
'StatusRaisedEvent',
|
|
421
|
+
'StatusResolvedEvent',
|
|
422
|
+
'TlsSecretBool',
|
|
423
|
+
'TlsSecretStr',
|
|
424
|
+
'UserSecretStr',
|
|
425
|
+
'build_model',
|
|
426
|
+
'diff',
|
|
427
|
+
'ensure_leader_for_app',
|
|
428
|
+
'gen_hash',
|
|
429
|
+
'gen_salt',
|
|
430
|
+
'get_encoded_dict',
|
|
431
|
+
'resource_added',
|
|
432
|
+
'store_new_data',
|
|
433
|
+
'write_model',
|
|
434
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
__version__ = '1.0.0'
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Copyright 2026 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Logic to compute diffs and modelling for diffs."""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from typing import Any, NamedTuple
|
|
18
|
+
|
|
19
|
+
from ops.model import Application, Relation, Unit
|
|
20
|
+
|
|
21
|
+
from dpcharmlibs.interfaces.utils import RESOURCE_ALIASES
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Diff(NamedTuple):
|
|
25
|
+
"""A tuple for storing the diff between two data mappings.
|
|
26
|
+
|
|
27
|
+
added - keys that were added
|
|
28
|
+
changed - keys that still exist but have new values
|
|
29
|
+
deleted - key that were deleted
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
added: set[str]
|
|
33
|
+
changed: set[str]
|
|
34
|
+
deleted: set[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def diff(old_data: dict[str, str] | None, new_data: dict[str, str]) -> Diff:
|
|
38
|
+
"""Retrieves the diff of the data in the relation changed databag for v1.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
old_data: dictionary of the stored data before the event.
|
|
42
|
+
new_data: dictionary of the received data to compute the diff.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
a Diff instance containing the added, deleted and changed
|
|
46
|
+
keys from the event relation databag.
|
|
47
|
+
"""
|
|
48
|
+
old_data = old_data or {}
|
|
49
|
+
|
|
50
|
+
# These are the keys that were added to the databag and triggered this event.
|
|
51
|
+
added = new_data.keys() - old_data.keys()
|
|
52
|
+
# These are the keys that were removed from the databag and triggered this event.
|
|
53
|
+
deleted = old_data.keys() - new_data.keys()
|
|
54
|
+
# These are the keys that already existed in the databag,
|
|
55
|
+
# but had their values changed.
|
|
56
|
+
changed = {key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]}
|
|
57
|
+
# Return the diff with all possible changes.
|
|
58
|
+
return Diff(added, changed, deleted)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resource_added(diff: Diff, aliases: list[str] | None = None) -> bool:
|
|
62
|
+
"""Ensures that one of the aliased resources has been added."""
|
|
63
|
+
all_aliases = RESOURCE_ALIASES + ['resource'] + (aliases or [])
|
|
64
|
+
return any(item in diff.added for item in all_aliases)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def store_new_data(
|
|
68
|
+
relation: Relation,
|
|
69
|
+
component: Unit | Application,
|
|
70
|
+
new_data: dict[str, str],
|
|
71
|
+
short_uuid: str | None = None,
|
|
72
|
+
global_data: dict[str, Any] | None = None,
|
|
73
|
+
):
|
|
74
|
+
"""Stores the new data in the databag for diff computation.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
relation: The relation considered to write data to
|
|
78
|
+
component: The component databag to write data to
|
|
79
|
+
new_data: a dictionary containing the data to write
|
|
80
|
+
short_uuid: Only present in V1, the request-id of that data to write.
|
|
81
|
+
global_data: request-independent, global state data to be written.
|
|
82
|
+
"""
|
|
83
|
+
global_data = global_data or {}
|
|
84
|
+
global_data = {k: v for k, v in global_data.items() if v}
|
|
85
|
+
# First, the case for V0
|
|
86
|
+
if not short_uuid:
|
|
87
|
+
relation.data[component].update({'data': json.dumps(new_data | global_data)})
|
|
88
|
+
# Then the case for V1, where we have a ShortUUID
|
|
89
|
+
else:
|
|
90
|
+
data = json.loads(relation.data[component].get('data', '{}')) | global_data
|
|
91
|
+
if not isinstance(data, dict):
|
|
92
|
+
raise ValueError
|
|
93
|
+
data[short_uuid] = new_data
|
|
94
|
+
relation.data[component].update({'data': json.dumps(data)})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Copyright 2026 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Exceptions."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataInterfacesError(Exception):
|
|
18
|
+
"""Common ancestor for DataInterfaces related exceptions."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SecretError(DataInterfacesError):
|
|
22
|
+
"""Common ancestor for Secrets related exceptions."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SecretAlreadyExistsError(SecretError):
|
|
26
|
+
"""A secret that was to be added already exists."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SecretsUnavailableError(SecretError):
|
|
30
|
+
"""Secrets aren't yet available for Juju version used."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IllegalOperationError(DataInterfacesError):
|
|
34
|
+
"""To be used when an operation is not allowed to be performed."""
|