Skip to content

Commit 0d03e8d

Browse files
authored
[MISC] Conditionally create the legacy interface to support Juju 4 (#368)
* Conditionally create the legacy interface to support Juju 4 * Update libs * Unhandled exception * Longer idle period * Increse timeout * Retry restart check * Disable lxd restart test
1 parent 127b1d8 commit 0d03e8d

File tree

3 files changed

+121
-23
lines changed

3 files changed

+121
-23
lines changed

lib/charms/data_platform_libs/v0/data_interfaces.py

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent):
331331

332332
# Increment this PATCH version before using `charmcraft publish-lib` or reset
333333
# to 0 if you are raising the major API version
334-
LIBPATCH = 46
334+
LIBPATCH = 49
335335

336336
PYDEPS = ["ops>=2.0.0"]
337337

@@ -2569,7 +2569,7 @@ def __init__(
25692569

25702570

25712571
################################################################################
2572-
# Cross-charm Relatoins Data Handling and Evenets
2572+
# Cross-charm Relations Data Handling and Events
25732573
################################################################################
25742574

25752575
# Generic events
@@ -3268,7 +3268,7 @@ def __init__(
32683268
# Kafka Events
32693269

32703270

3271-
class KafkaProvidesEvent(RelationEvent):
3271+
class KafkaProvidesEvent(RelationEventWithSecret):
32723272
"""Base class for Kafka events."""
32733273

32743274
@property
@@ -3287,6 +3287,40 @@ def consumer_group_prefix(self) -> Optional[str]:
32873287

32883288
return self.relation.data[self.relation.app].get("consumer-group-prefix")
32893289

3290+
@property
3291+
def mtls_cert(self) -> Optional[str]:
3292+
"""Returns TLS cert of the client."""
3293+
if not self.relation.app:
3294+
return None
3295+
3296+
if not self.secrets_enabled:
3297+
raise SecretsUnavailableError("Secrets unavailable on current Juju version")
3298+
3299+
secret_field = f"{PROV_SECRET_PREFIX}{SECRET_GROUPS.MTLS}"
3300+
if secret_uri := self.relation.data[self.app].get(secret_field):
3301+
secret = self.framework.model.get_secret(id=secret_uri)
3302+
content = secret.get_content(refresh=True)
3303+
if content:
3304+
return content.get("mtls-cert")
3305+
3306+
3307+
class KafkaClientMtlsCertUpdatedEvent(KafkaProvidesEvent):
3308+
"""Event emitted when the mtls relation is updated."""
3309+
3310+
def __init__(self, handle, relation, old_mtls_cert: Optional[str] = None, app=None, unit=None):
3311+
super().__init__(handle, relation, app, unit)
3312+
3313+
self.old_mtls_cert = old_mtls_cert
3314+
3315+
def snapshot(self):
3316+
"""Return a snapshot of the event."""
3317+
return super().snapshot() | {"old_mtls_cert": self.old_mtls_cert}
3318+
3319+
def restore(self, snapshot):
3320+
"""Restore the event from a snapshot."""
3321+
super().restore(snapshot)
3322+
self.old_mtls_cert = snapshot["old_mtls_cert"]
3323+
32903324

32913325
class TopicRequestedEvent(KafkaProvidesEvent, ExtraRoleEvent):
32923326
"""Event emitted when a new topic is requested for use on this relation."""
@@ -3299,6 +3333,7 @@ class KafkaProvidesEvents(CharmEvents):
32993333
"""
33003334

33013335
topic_requested = EventSource(TopicRequestedEvent)
3336+
mtls_cert_updated = EventSource(KafkaClientMtlsCertUpdatedEvent)
33023337

33033338

33043339
class KafkaRequiresEvent(RelationEvent):
@@ -3416,6 +3451,13 @@ def __init__(self, charm: CharmBase, relation_data: KafkaProviderData) -> None:
34163451
def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
34173452
"""Event emitted when the relation has changed."""
34183453
super()._on_relation_changed_event(event)
3454+
3455+
new_data_keys = list(event.relation.data[event.app].keys())
3456+
if any(newval for newval in new_data_keys if self.relation_data._is_secret_field(newval)):
3457+
self.relation_data._register_secrets_to_relation(event.relation, new_data_keys)
3458+
3459+
getattr(self.on, "mtls_cert_updated").emit(event.relation, app=event.app, unit=event.unit)
3460+
34193461
# Leader only
34203462
if not self.relation_data.local_unit.is_leader():
34213463
return
@@ -3430,6 +3472,33 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
34303472
event.relation, app=event.app, unit=event.unit
34313473
)
34323474

3475+
def _on_secret_changed_event(self, event: SecretChangedEvent):
3476+
"""Event notifying about a new value of a secret."""
3477+
if not event.secret.label:
3478+
return
3479+
3480+
relation = self.relation_data._relation_from_secret_label(event.secret.label)
3481+
if not relation:
3482+
logging.info(
3483+
f"Received secret {event.secret.label} but couldn't parse, seems irrelevant"
3484+
)
3485+
return
3486+
3487+
if relation.app == self.charm.app:
3488+
logging.info("Secret changed event ignored for Secret Owner")
3489+
3490+
remote_unit = None
3491+
for unit in relation.units:
3492+
if unit.app != self.charm.app:
3493+
remote_unit = unit
3494+
3495+
old_mtls_cert = event.secret.get_content().get("mtls-cert")
3496+
# mtls-cert is the only secret that can be updated
3497+
logger.info("mtls-cert updated")
3498+
getattr(self.on, "mtls_cert_updated").emit(
3499+
relation, app=relation.app, unit=remote_unit, old_mtls_cert=old_mtls_cert
3500+
)
3501+
34333502

34343503
class KafkaProvides(KafkaProviderData, KafkaProviderEventHandlers):
34353504
"""Provider-side of the Kafka relation."""
@@ -3450,11 +3519,18 @@ def __init__(
34503519
extra_user_roles: Optional[str] = None,
34513520
consumer_group_prefix: Optional[str] = None,
34523521
additional_secret_fields: Optional[List[str]] = [],
3522+
mtls_cert: Optional[str] = None,
34533523
):
34543524
"""Manager of Kafka client relations."""
34553525
super().__init__(model, relation_name, extra_user_roles, additional_secret_fields)
34563526
self.topic = topic
34573527
self.consumer_group_prefix = consumer_group_prefix or ""
3528+
self.mtls_cert = mtls_cert
3529+
3530+
@staticmethod
3531+
def is_topic_value_acceptable(topic_value: str) -> bool:
3532+
"""Check whether the given Kafka topic value is acceptable."""
3533+
return "*" not in topic_value[:3]
34583534

34593535
@property
34603536
def topic(self):
@@ -3463,11 +3539,19 @@ def topic(self):
34633539

34643540
@topic.setter
34653541
def topic(self, value):
3466-
# Avoid wildcards
3467-
if value == "*":
3468-
raise ValueError(f"Error on topic '{value}', cannot be a wildcard.")
3542+
if not self.is_topic_value_acceptable(value):
3543+
raise ValueError(f"Error on topic '{value}', unacceptable value.")
34693544
self._topic = value
34703545

3546+
def set_mtls_cert(self, relation_id: int, mtls_cert: str) -> None:
3547+
"""Set the mtls cert in the application relation databag / secret.
3548+
3549+
Args:
3550+
relation_id: the identifier for a particular relation.
3551+
mtls_cert: mtls cert.
3552+
"""
3553+
self.update_relation_data(relation_id, {"mtls-cert": mtls_cert})
3554+
34713555

34723556
class KafkaRequirerEventHandlers(RequirerEventHandlers):
34733557
"""Requires-side of the Kafka relation."""
@@ -3489,6 +3573,9 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None:
34893573
# Sets topic, extra user roles, and "consumer-group-prefix" in the relation
34903574
relation_data = {"topic": self.relation_data.topic}
34913575

3576+
if self.relation_data.mtls_cert:
3577+
relation_data["mtls-cert"] = self.relation_data.mtls_cert
3578+
34923579
if self.relation_data.extra_user_roles:
34933580
relation_data["extra-user-roles"] = self.relation_data.extra_user_roles
34943581

@@ -3547,15 +3634,17 @@ def __init__(
35473634
extra_user_roles: Optional[str] = None,
35483635
consumer_group_prefix: Optional[str] = None,
35493636
additional_secret_fields: Optional[List[str]] = [],
3637+
mtls_cert: Optional[str] = None,
35503638
) -> None:
35513639
KafkaRequirerData.__init__(
35523640
self,
35533641
charm.model,
35543642
relation_name,
35553643
topic,
3556-
extra_user_roles,
3557-
consumer_group_prefix,
3558-
additional_secret_fields,
3644+
extra_user_roles=extra_user_roles,
3645+
consumer_group_prefix=consumer_group_prefix,
3646+
additional_secret_fields=additional_secret_fields,
3647+
mtls_cert=mtls_cert,
35593648
)
35603649
KafkaRequirerEventHandlers.__init__(self, charm, self)
35613650

@@ -3675,6 +3764,10 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
36753764
event.relation, app=event.app, unit=event.unit
36763765
)
36773766

3767+
def _on_secret_changed_event(self, event: SecretChangedEvent) -> None:
3768+
"""Event emitted when the relation data has changed."""
3769+
pass
3770+
36783771

36793772
class OpenSearchProvides(OpenSearchProvidesData, OpenSearchProvidesEventHandlers):
36803773
"""Provider-side of the OpenSearch relation."""

src/charm.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727

2828
logger = logging.getLogger(__name__)
2929

30-
pgsql = ops.lib.use("pgsql", 1, "postgresql-charmers@lists.launchpad.net")
31-
3230
PEER = "postgresql-test-peers"
3331
# Expected tmp access
3432
LAST_WRITTEN_FILE = "/tmp/last_written_value" # noqa: S108
@@ -151,10 +149,12 @@ def __init__(self, *args):
151149
self.no_database = DatabaseRequires(self, "no-database", database_name="")
152150

153151
# Legacy interface
154-
self.db = pgsql.PostgreSQLClient(self, "db")
155-
self.framework.observe(
156-
self.db.on.database_relation_joined, self._on_database_relation_joined
157-
)
152+
if self.model.juju_version.major < 4:
153+
pgsql = ops.lib.use("pgsql", 1, "postgresql-charmers@lists.launchpad.net")
154+
self.db = pgsql.PostgreSQLClient(self, "db")
155+
self.framework.observe(
156+
self.db.on.database_relation_joined, self._on_database_relation_joined
157+
)
158158

159159
self.framework.observe(self.on.run_sql_action, self._on_run_sql_action)
160160
self.framework.observe(self.on.test_tls_action, self._on_test_tls_action)
@@ -338,6 +338,7 @@ def _on_start_continuous_writes_action(self, event: ActionEvent) -> None:
338338
logger.exception("Unable to stop writes to create table", exc_info=e)
339339
return
340340

341+
connection = None
341342
try:
342343
# Create the table to write records on and also a unique index to prevent duplicate
343344
# writes.
@@ -355,12 +356,14 @@ def _on_start_continuous_writes_action(self, event: ActionEvent) -> None:
355356
logger.exception("Unable to create table", exc_info=e)
356357
return
357358
finally:
358-
connection.close()
359+
if connection:
360+
connection.close()
359361

360362
self._start_continuous_writes(1)
361363
event.set_results({"result": "True"})
362364

363365
def _get_db_writes(self) -> int:
366+
connection = None
364367
try:
365368
with (
366369
psycopg2.connect(self._connection_string) as connection,
@@ -373,7 +376,8 @@ def _get_db_writes(self) -> int:
373376
writes = -1
374377
logger.exception("Unable to count writes")
375378
finally:
376-
connection.close()
379+
if connection:
380+
connection.close()
377381
return writes
378382

379383
def _on_show_continuous_writes_action(self, event: ActionEvent) -> None:
@@ -436,10 +440,7 @@ def _stop_continuous_writes(self) -> int | None:
436440
return last_written_value
437441

438442
# Legacy event handlers
439-
def _on_database_relation_joined(
440-
self,
441-
event: pgsql.DatabaseRelationJoinedEvent, # type: ignore
442-
) -> None:
443+
def _on_database_relation_joined(self, event) -> None:
443444
"""Handle db-relation-joined.
444445
445446
Args:

tests/integration/test_smoke.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import time
88

9+
import pytest
910
from juju.relation import Relation
1011
from lightkube.core.client import Client
1112
from lightkube.resources.core_v1 import Pod
@@ -63,7 +64,7 @@ async def test_smoke(ops_test: OpsTest, charm) -> None:
6364
await integrate(ops_test, postgresql, pgbouncer)
6465
await integrate(ops_test, f"{TEST_APP_NAME}:database", pgbouncer)
6566
await ops_test.model.wait_for_idle(
66-
apps=[postgresql, pgbouncer, TEST_APP_NAME], status="active", timeout=1000
67+
apps=[postgresql, pgbouncer, TEST_APP_NAME], status="active", timeout=1000, idle_period=30
6768
)
6869

6970
logger.info("Test continuous writes")
@@ -142,11 +143,14 @@ async def test_restart(ops_test: OpsTest) -> None:
142143
client = Client(namespace=ops_test.model.info.name)
143144
client.delete(Pod, name=f"{TEST_APP_NAME}-0")
144145
else:
146+
pytest.skip("Unstable LXC restart test")
145147
logger.info("Restarting lxc")
146148
await restart_machine(ops_test, ops_test.model.applications[TEST_APP_NAME].units[0].name)
147149

148150
logger.info("Wait for idle")
149-
await ops_test.model.wait_for_idle(apps=[TEST_APP_NAME], status="active", timeout=600)
151+
await ops_test.model.wait_for_idle(
152+
apps=[TEST_APP_NAME], status="active", timeout=600, idle_period=30
153+
)
150154

151155
logger.info("Check that writes are increasing")
152156
results = await (

0 commit comments

Comments
 (0)