Skip to content

Commit b4105ed

Browse files
authored
[DPE-7512] Multinode deployment (#3)
* Multinode deployment. * Add collect_app_status handler and config integration test. * Ger rid of collect_status in charm.py. * Review suggestions. * Review suggestions. * Add spread tests. * Set UnitWorkloadState.ACTIVE only after unit actually ready. Change ClusterState.active -> is_active. Suppress exec error logging during is_healthy check. * Increase timeout for multinode deployment. * Documentation * Documentation. * Documentation. * Fix unit tests. * Fix unit tests, add ops-scenario. * Documentation. * Bootstrap using rolling ops. * Fix unit tests. * Improve bootstrap logic. Izolate config reading only to start and config changed events. Add state to the config manager. * Wait exponentially with timeout of 30 minutes in bootstrap process. * Improve unit tests. Fix invalid config early exit in start event.
1 parent bfcd15e commit b4105ed

File tree

30 files changed

+1226
-455
lines changed

30 files changed

+1226
-455
lines changed

CONTRIBUTING.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Contributing
1+
# Contributing workflow
22

33
To make contributions to this charm, you'll need a working [development setup](https://juju.is/docs/sdk/dev-setup).
44

5-
You can create an environment for development with `tox`:
5+
You can, but not required to, create an environment for development with `tox`:
66

77
```shell
88
tox devenv -e integration
@@ -12,15 +12,16 @@ source venv/bin/activate
1212
## Testing
1313

1414
This project uses `tox` for managing test environments. There are some pre-configured environments
15-
that can be used for linting and formatting code when you're preparing contributions to the charm:
15+
that can be used for linting, formatting and testing code when you're preparing contributions to the charm:
1616

1717
```shell
18-
tox run -e format # update your code according to linting rules
19-
tox run -e lint # code style
20-
tox run -e static # static type checking
21-
tox run -e unit # unit tests
22-
tox run -e integration # integration tests
23-
tox # runs 'format', 'lint', 'static', and 'unit' environments
18+
tox run -e format # update your code according to linting rules
19+
tox run -e lint # verify your code according to linting rules
20+
tox run -e unit # run unit tests
21+
tox run -e integration-charm # run charm integration test
22+
tox run -e integration-config # run config integration test
23+
tox run -e integration-multinode # run multinode integration test
24+
tox run -e integration-scaling # run scaling integration test
2425
```
2526

2627
## Build the charm

charmcraft.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ charm-libs:
2020
version: "1"
2121
- lib: data_platform_libs.data_interfaces
2222
version: "0"
23+
- lib: rolling_ops.rollingops
24+
version: "0"
2325

2426
config:
2527
options:
@@ -35,6 +37,8 @@ config:
3537
peers:
3638
cassandra-peers:
3739
interface: cassandra_peers
40+
bootstrap:
41+
interface: rolling_op
3842

3943
parts:
4044
# "poetry-deps" part name is a magic constant

lib/charms/data_platform_libs/v0/data_interfaces.py

Lines changed: 95 additions & 6 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 = 48
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,13 @@ 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
34583529

34593530
@property
34603531
def topic(self):
@@ -3468,6 +3539,15 @@ def topic(self, value):
34683539
raise ValueError(f"Error on topic '{value}', cannot be a wildcard.")
34693540
self._topic = value
34703541

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

34723552
class KafkaRequirerEventHandlers(RequirerEventHandlers):
34733553
"""Requires-side of the Kafka relation."""
@@ -3489,6 +3569,9 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None:
34893569
# Sets topic, extra user roles, and "consumer-group-prefix" in the relation
34903570
relation_data = {"topic": self.relation_data.topic}
34913571

3572+
if self.relation_data.mtls_cert:
3573+
relation_data["mtls-cert"] = self.relation_data.mtls_cert
3574+
34923575
if self.relation_data.extra_user_roles:
34933576
relation_data["extra-user-roles"] = self.relation_data.extra_user_roles
34943577

@@ -3547,15 +3630,17 @@ def __init__(
35473630
extra_user_roles: Optional[str] = None,
35483631
consumer_group_prefix: Optional[str] = None,
35493632
additional_secret_fields: Optional[List[str]] = [],
3633+
mtls_cert: Optional[str] = None,
35503634
) -> None:
35513635
KafkaRequirerData.__init__(
35523636
self,
35533637
charm.model,
35543638
relation_name,
35553639
topic,
3556-
extra_user_roles,
3557-
consumer_group_prefix,
3558-
additional_secret_fields,
3640+
extra_user_roles=extra_user_roles,
3641+
consumer_group_prefix=consumer_group_prefix,
3642+
additional_secret_fields=additional_secret_fields,
3643+
mtls_cert=mtls_cert,
35593644
)
35603645
KafkaRequirerEventHandlers.__init__(self, charm, self)
35613646

@@ -3675,6 +3760,10 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
36753760
event.relation, app=event.app, unit=event.unit
36763761
)
36773762

3763+
def _on_secret_changed_event(self, event: SecretChangedEvent) -> None:
3764+
"""Event emitted when the relation data has changed."""
3765+
pass
3766+
36783767

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

0 commit comments

Comments
 (0)