Skip to content

Commit ef78f52

Browse files
committed
feat: DPV1
1 parent 1927fac commit ef78f52

File tree

25 files changed

+15794
-402
lines changed

25 files changed

+15794
-402
lines changed

.github/workflows/ci.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,6 @@ jobs:
167167
# Update whenever charmcraft.yaml is changed
168168
- series: jammy
169169
bases-index: 0
170-
- series: noble
171-
bases-index: 1
172170
tox-environments:
173171
- integration-db-v1
174172
- integration-opensearch-v1

.gitignore

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,24 @@ build/
55
.coverage
66
__pycache__/
77
*.py[cod]
8-
tests/integration/application-charm/lib/charms/data_platform_libs/v0/database_requires.py
9-
tests/integration/database-charm/lib/charms/data_platform_libs/v0/database_provides.py
10-
tests/integration/application-charm/lib/charms/data_platform_libs/v0/data_interfaces.py
11-
tests/integration/application-s3-charm/lib/charms/data_platform_libs/v0/s3.py
12-
tests/integration/database-charm/lib/charms/data_platform_libs/v0/data_interfaces.py
13-
tests/integration/kafka-charm/lib/charms/data_platform_libs/v0/data_interfaces.py
14-
tests/integration/s3-charm/lib/charms/data_platform_libs/v0/s3.py'
8+
tests/v0/integration/application-charm/lib/charms/data_platform_libs/v0/database_requires.py
9+
tests/v0/integration/database-charm/lib/charms/data_platform_libs/v0/database_provides.py
10+
tests/v0/integration/application-charm/lib/charms/data_platform_libs/v0/data_interfaces.py
11+
tests/v0/integration/application-s3-charm/lib/charms/data_platform_libs/v0/s3.py
12+
tests/v0/integration/database-charm/lib/charms/data_platform_libs/v0/data_interfaces.py
13+
tests/v0/integration/kafka-charm/lib/charms/data_platform_libs/v0/data_interfaces.py
14+
tests/v0/integration/s3-charm/lib/charms/data_platform_libs/v0/s3.py
15+
tests/v1/integration/application-charm/lib/charms/data_platform_libs/v1/database_requires.py
16+
tests/v1/integration/database-charm/lib/charms/data_platform_libs/v1/database_provides.py
17+
tests/v1/integration/application-charm/lib/charms/data_platform_libs/v1/data_interfaces.py
18+
tests/v1/integration/application-s3-charm/lib/charms/data_platform_libs/v1/s3.py
19+
tests/v1/integration/database-charm/lib/charms/data_platform_libs/v1/data_interfaces.py
20+
tests/v1/integration/kafka-charm/lib/charms/data_platform_libs/v1/data_interfaces.py
21+
tests/v1/integration/s3-charm/lib/charms/data_platform_libs/v1/s3.py'
22+
tests/v1/integration/backward-compatibility-charm/lib/charms/data_platform_libs/v0/data_interfaces.py
23+
tests/v1/integration/backward-compatibility-charm/lib/charms/data_platform_libs/v1/data_interfaces.py
24+
tests/v1/integration/opensearch-charm/lib/charms/data_platform_libs/v1/data_interfaces.py
25+
tests/v1/integration/kafka-connect-charm/lib/charms/data_platform_libs/v1/data_interfaces.py
26+
tests/v1/integration/dummy-database-charm/lib/charms/data_platform_libs/v1/data_interfaces.py
1527
.vscode/
1628
.idea/

lib/charms/data_platform_libs/v1/data_interfaces.py

Lines changed: 118 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,9 @@ def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
287287
from typing_extensions import TypeAliasType, override
288288

289289
try:
290-
import psycopg
290+
import psycopg2
291291
except ImportError:
292-
psycopg = None
292+
psycopg2 = None
293293

294294
# The unique Charmhub library identifier, never change it
295295
LIBID = "6c3e6b6680d64e9c89e611d1a15f65be"
@@ -654,6 +654,7 @@ class PeerModel(BaseModel):
654654
populate_by_name=True,
655655
serialize_by_alias=True,
656656
alias_generator=lambda x: x.replace("_", "-"),
657+
extra="allow",
657658
)
658659

659660
@model_validator(mode="after")
@@ -680,6 +681,8 @@ def extract_secrets(self, info: ValidationInfo):
680681

681682
if value and field_info.annotation == OptionalSecretBool:
682683
value = SecretBool(json.loads(value))
684+
elif value:
685+
value = SecretStr(value)
683686
setattr(self, field, value)
684687

685688
return self
@@ -706,9 +709,11 @@ def serialize_model(self, handler: SerializerFunctionWrapHandler, info: Serializ
706709
actual_value = (
707710
value.get_secret_value() if issubclass(value.__class__, _SecretBase) else value
708711
)
712+
if not isinstance(actual_value, str):
713+
actual_value = json.dumps(actual_value)
709714

710715
if secret is None:
711-
if actual_value:
716+
if value:
712717
secret = repository.add_secret(
713718
aliased_field,
714719
actual_value,
@@ -721,11 +726,9 @@ def serialize_model(self, handler: SerializerFunctionWrapHandler, info: Serializ
721726
content = secret.get_content()
722727
full_content = copy.deepcopy(content)
723728

724-
if actual_value is None:
725-
full_content.pop(field, None)
729+
if value is None:
730+
full_content.pop(aliased_field, None)
726731
else:
727-
if not isinstance(actual_value, str):
728-
actual_value = json.dumps(actual_value)
729732
full_content.update({aliased_field: actual_value})
730733
secret.set_content(full_content)
731734
return handler(self)
@@ -744,6 +747,7 @@ class CommonModel(BaseModel):
744747
populate_by_name=True,
745748
serialize_by_alias=True,
746749
alias_generator=lambda x: x.replace("_", "-"),
750+
extra="allow",
747751
)
748752

749753
resource: str = Field(validation_alias=AliasChoices(*RESOURCE_ALIASES), default="")
@@ -787,6 +791,9 @@ def extract_secrets(self, info: ValidationInfo):
787791
value = secret.get_content().get(aliased_field)
788792
if value and field_info.annotation == OptionalSecretBool:
789793
value = SecretBool(json.loads(value))
794+
elif value:
795+
value = SecretStr(value)
796+
790797
setattr(self, field, value)
791798
return self
792799

@@ -822,9 +829,11 @@ def serialize_model(self, handler: SerializerFunctionWrapHandler, info: Serializ
822829
actual_value = (
823830
value.get_secret_value() if issubclass(value.__class__, _SecretBase) else value
824831
)
832+
if not isinstance(actual_value, str):
833+
actual_value = json.dumps(actual_value)
825834

826835
if secret is None:
827-
if actual_value:
836+
if value:
828837
secret = repository.add_secret(
829838
aliased_field, actual_value, secret_group, short_uuid
830839
)
@@ -836,12 +845,10 @@ def serialize_model(self, handler: SerializerFunctionWrapHandler, info: Serializ
836845
content = secret.get_content()
837846
full_content = copy.deepcopy(content)
838847

839-
if actual_value is None:
840-
full_content.pop(field, None)
848+
if value is None:
849+
full_content.pop(aliased_field, None)
841850
_encountered_secrets.add((secret, secret_field))
842851
else:
843-
if not isinstance(actual_value, str):
844-
actual_value = json.dumps(actual_value)
845852
full_content.update({aliased_field: actual_value})
846853
secret.set_content(full_content)
847854

@@ -1003,10 +1010,11 @@ class DataContractV1(BaseModel, Generic[TResourceProviderModel]):
10031010
TCommonModel = TypeVar("TCommonModel", bound=CommonModel)
10041011

10051012

1006-
def is_topic_value_acceptable(value: str | None):
1013+
def is_topic_value_acceptable(value: str | None) -> str | None:
10071014
"""Check whether the given Kafka topic value is acceptable."""
10081015
if value and "*" in value[:3]:
10091016
raise ValueError(f"Error on topic '{value}',, unacceptable value.")
1017+
return value
10101018

10111019

10121020
class KafkaRequestModel(RequirerCommonModel):
@@ -1709,9 +1717,12 @@ def write_model(
17091717
"""Writes the data stored in the model using the repository object."""
17101718
context = context or {}
17111719
dumped = model.model_dump(
1712-
mode="json", context={"repository": repository} | context, exclude_none=True
1720+
mode="json", context={"repository": repository} | context, exclude_none=False
17131721
)
17141722
for field, value in dumped.items():
1723+
if value is None:
1724+
repository.delete_field(field)
1725+
continue
17151726
dumped_value = value if isinstance(value, str) else json.dumps(value)
17161727
repository.write_field(field, dumped_value)
17171728

@@ -1951,13 +1962,19 @@ class ResourceEntityCreatedEvent(ResourceRequirerEvent[TResourceProviderModel]):
19511962

19521963

19531964
class ResourceEndpointsChangedEvent(ResourceRequirerEvent[TResourceProviderModel]):
1954-
"""Read/Write enpoinds are changed."""
1965+
"""Read/Write enpoints are changed."""
19551966

19561967
pass
19571968

19581969

19591970
class ResourceReadOnlyEndpointsChangedEvent(ResourceRequirerEvent[TResourceProviderModel]):
1960-
"""Read-only enpoinds are changed."""
1971+
"""Read-only enpoints are changed."""
1972+
1973+
pass
1974+
1975+
1976+
class AuthenticationUpdatedEvent(ResourceRequirerEvent[TResourceProviderModel]):
1977+
"""Authentication was updated for a user."""
19611978

19621979
pass
19631980

@@ -1972,6 +1989,7 @@ class ResourceRequiresEvents(CharmEvents, Generic[TResourceProviderModel]):
19721989
resource_entity_created = EventSource(ResourceEntityCreatedEvent)
19731990
endpoints_changed = EventSource(ResourceEndpointsChangedEvent)
19741991
read_only_endpoints_changed = EventSource(ResourceReadOnlyEndpointsChangedEvent)
1992+
authentication_updated = EventSource(AuthenticationUpdatedEvent)
19751993

19761994

19771995
##############################################################################
@@ -2072,6 +2090,34 @@ def compute_diff(
20722090

20732091
return _diff
20742092

2093+
def _relation_from_secret_label(self, secret_label: str) -> Relation | None:
2094+
"""Retrieve the relation that belongs to a secret label."""
2095+
contents = secret_label.split(".")
2096+
2097+
if not (contents and len(contents) >= 3):
2098+
return
2099+
2100+
try:
2101+
relation_id = int(contents[1])
2102+
except ValueError:
2103+
return
2104+
2105+
relation_name = contents[0]
2106+
2107+
try:
2108+
return self.model.get_relation(relation_name, relation_id)
2109+
except ModelError:
2110+
return
2111+
2112+
def _short_uuid_from_secret_label(self, secret_label: str) -> str | None:
2113+
"""Retrieve the relation that belongs to a secret label."""
2114+
contents = secret_label.split(".")
2115+
2116+
if not (contents and len(contents) >= 5):
2117+
return
2118+
2119+
return contents[2]
2120+
20752121

20762122
class ResourceProviderEventHandler(EventHandlers, Generic[TRequirerCommonModel]):
20772123
"""Event Handler for resource provider."""
@@ -2204,34 +2250,6 @@ def _handle_bulk_event(
22042250
)
22052251
store_new_data(event.relation, self.component, new_data, request.request_id)
22062252

2207-
def _relation_from_secret_label(self, secret_label: str) -> Relation | None:
2208-
"""Retrieve the relation that belongs to a secret label."""
2209-
contents = secret_label.split(".")
2210-
2211-
if not (contents and len(contents) >= 3):
2212-
return
2213-
2214-
try:
2215-
relation_id = int(contents[1])
2216-
except ValueError:
2217-
return
2218-
2219-
relation_name = contents[0]
2220-
2221-
try:
2222-
return self.model.get_relation(relation_name, relation_id)
2223-
except ModelError:
2224-
return
2225-
2226-
def _short_uuid_from_secret_label(self, secret_label: str) -> str | None:
2227-
"""Retrieve the relation that belongs to a secret label."""
2228-
contents = secret_label.split(".")
2229-
2230-
if not (contents and len(contents) >= 5):
2231-
return
2232-
2233-
return contents[2]
2234-
22352253
@override
22362254
def _on_secret_changed_event(self, event: SecretChangedEvent) -> None:
22372255
if not self.mtls_enabled:
@@ -2251,6 +2269,11 @@ def _on_secret_changed_event(self, event: SecretChangedEvent) -> None:
22512269

22522270
if relation.app == self.charm.app:
22532271
logging.info("Secret changed event ignored for Secret Owner")
2272+
return
2273+
2274+
if relation.name != self.relation_name:
2275+
logging.info("Secret changed on wrong relation.")
2276+
return
22542277

22552278
remote_unit = None
22562279
for unit in relation.units:
@@ -2470,39 +2493,39 @@ def are_all_resources_created(self, rel_id: int) -> bool:
24702493
def _is_pg_plugin_enabled(plugin: str, connection_string: str) -> bool:
24712494
# Actual checking method.
24722495
# No need to check for psycopg here, it's been checked before.
2473-
if not psycopg:
2496+
if not psycopg2:
24742497
return False
24752498

24762499
try:
2477-
with psycopg.connect(connection_string) as connection:
2500+
with psycopg2.connect(connection_string) as connection:
24782501
with connection.cursor() as cursor:
24792502
cursor.execute(
24802503
"SELECT TRUE FROM pg_extension WHERE extname=%s::text;", (plugin,)
24812504
)
24822505
return cursor.fetchone() is not None
2483-
except psycopg.Error as e:
2506+
except psycopg2.Error as e:
24842507
logger.exception(
24852508
f"failed to check whether {plugin} plugin is enabled in the database: %s",
24862509
str(e),
24872510
)
24882511
return False
24892512

2490-
def is_postgresql_plugin_enabled(self, plugin: str, relation_id: int = 0) -> bool:
2513+
def is_postgresql_plugin_enabled(self, plugin: str, relation_index: int = 0) -> bool:
24912514
"""Returns whether a plugin is enabled in the database.
24922515
24932516
Args:
24942517
plugin: name of the plugin to check.
2495-
relation_id: Optional index to check the database (default: 0 - first relation).
2518+
relation_index: Optional index to check the database (default: 0 - first relation).
24962519
"""
2497-
if not psycopg:
2520+
if not psycopg2:
24982521
return False
24992522

25002523
# Can't check a non existing relation.
2501-
if len(self.relations) <= relation_id:
2524+
if len(self.relations) <= relation_index:
25022525
return False
25032526

2504-
relation_id = self.relations[relation_id].id
2505-
model = self.interface.build_model(relation_id=relation_id)
2527+
relation = self.relations[relation_index]
2528+
model = self.interface.build_model(relation_id=relation.id, component=relation.app)
25062529
for request in model.requests:
25072530
if request.endpoints and request.username and request.password:
25082531
host = request.endpoints.split(":")[0]
@@ -2577,7 +2600,48 @@ def _get_relation_alias(self, relation_id: int) -> str | None:
25772600

25782601
def _on_secret_changed_event(self, event: SecretChangedEvent):
25792602
"""Event notifying about a new value of a secret."""
2580-
pass
2603+
if not event.secret.label:
2604+
return
2605+
relation = self._relation_from_secret_label(event.secret.label)
2606+
short_uuid = self._short_uuid_from_secret_label(event.secret.label)
2607+
2608+
if not relation:
2609+
logging.info(
2610+
f"Received secret {event.secret.label} but couldn't parse, seems irrelevant"
2611+
)
2612+
return
2613+
2614+
if relation.app == self.charm.app:
2615+
logging.info("Secret changed event ignored for Secret Owner")
2616+
return
2617+
2618+
if relation.name != self.relation_name:
2619+
logging.info("Secret changed on wrong relation.")
2620+
return
2621+
2622+
remote_unit = None
2623+
for unit in relation.units:
2624+
if unit.app != self.charm.app:
2625+
remote_unit = unit
2626+
break
2627+
2628+
response_model = self.interface.build_model(relation.id)
2629+
if not short_uuid:
2630+
return
2631+
for _response in response_model.requests:
2632+
if _response.request_id == short_uuid:
2633+
response = _response
2634+
break
2635+
else:
2636+
logger.info(f"Unknown request id {short_uuid}")
2637+
return
2638+
2639+
getattr(self.on, "authentication_updated").emit(
2640+
relation,
2641+
app=relation.app,
2642+
unit=remote_unit,
2643+
response=response,
2644+
)
25812645

25822646
def _on_relation_created_event(self, event: RelationCreatedEvent) -> None:
25832647
"""Event emitted when the database relation is created."""

0 commit comments

Comments
 (0)