Skip to content

Commit f9ea24c

Browse files
[DPE-5249] Add integration support for hacluster (#177)
## Issue We need to add integration support for the hacluster charm. This will allow the mysqlrouter charm to be exposed via data-integrator through a provided virtual IP ## Solution Add integration support
1 parent 04c7b3a commit f9ea24c

File tree

14 files changed

+654
-40
lines changed

14 files changed

+654
-40
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ jobs:
130130
exclude:
131131
- groups: {path_to_test_file: tests/integration/test_data_integrator.py}
132132
ubuntu-versions: {series: focal}
133+
- groups: {path_to_test_file: tests/integration/test_hacluster.py}
134+
ubuntu-versions: {series: focal}
133135
name: ${{ matrix.juju-snap-channel }} - (GH hosted) ${{ matrix.groups.job_name }} | ${{ matrix.ubuntu-versions.series }}
134136
needs:
135137
- lint

charmcraft.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ bases:
1616
channel: "22.04"
1717
architectures: [arm64]
1818
parts:
19+
files:
20+
plugin: dump
21+
source: .
22+
prime:
23+
- charm_version
24+
- workload_version
1925
charm:
2026
override-pull: |
2127
craftctl default
@@ -27,9 +33,6 @@ parts:
2733
# TODO: enable after https://github.com/canonical/charmcraft/issues/1456 fixed
2834
charm-strict-dependencies: false
2935
charm-entrypoint: src/machine_charm.py
30-
prime:
31-
- charm_version
32-
- workload_version
3336
build-packages:
3437
- libffi-dev
3538
- libssl-dev

config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright 2024 Canonical Ltd.
2+
# See LICENSE file for licensing details.
3+
4+
options:
5+
6+
vip:
7+
description: |
8+
Virtual IP to use to front mysql router units. Used only in case of external node connection.
9+
type: string

lib/charms/tempo_k8s/v2/tracing.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def __init__(self, *args):
9797
)
9898
from ops.framework import EventSource, Object
9999
from ops.model import ModelError, Relation
100-
from pydantic import BaseModel, ConfigDict, Field
100+
from pydantic import BaseModel, Field
101101

102102
# The unique Charmhub library identifier, never change it
103103
LIBID = "12977e9aa0b34367903d8afeb8c3d85d"
@@ -107,7 +107,7 @@ def __init__(self, *args):
107107

108108
# Increment this PATCH version before using `charmcraft publish-lib` or reset
109109
# to 0 if you are raising the major API version
110-
LIBPATCH = 8
110+
LIBPATCH = 10
111111

112112
PYDEPS = ["pydantic"]
113113

@@ -338,7 +338,7 @@ class Config:
338338
class ProtocolType(BaseModel):
339339
"""Protocol Type."""
340340

341-
model_config = ConfigDict(
341+
model_config = ConfigDict( # type: ignore
342342
# Allow serializing enum values.
343343
use_enum_values=True
344344
)
@@ -902,7 +902,16 @@ def _get_endpoint(
902902
def get_endpoint(
903903
self, protocol: ReceiverProtocol, relation: Optional[Relation] = None
904904
) -> Optional[str]:
905-
"""Receiver endpoint for the given protocol."""
905+
"""Receiver endpoint for the given protocol.
906+
907+
It could happen that this function gets called before the provider publishes the endpoints.
908+
In such a scenario, if a non-leader unit calls this function, a permission denied exception will be raised due to
909+
restricted access. To prevent this, this function needs to be guarded by the `is_ready` check.
910+
911+
Raises:
912+
ProtocolNotRequestedError:
913+
If the charm unit is the leader unit and attempts to obtain an endpoint for a protocol it did not request.
914+
"""
906915
endpoint = self._get_endpoint(relation or self._relation, protocol=protocol)
907916
if not endpoint:
908917
requested_protocols = set()
@@ -925,7 +934,7 @@ def get_endpoint(
925934
def charm_tracing_config(
926935
endpoint_requirer: TracingEndpointRequirer, cert_path: Optional[Union[Path, str]]
927936
) -> Tuple[Optional[str], Optional[str]]:
928-
"""Utility function to determine the charm_tracing config you will likely want.
937+
"""Return the charm_tracing config you likely want.
929938
930939
If no endpoint is provided:
931940
disable charm tracing.

metadata.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ requires:
4646
interface: tracing
4747
optional: true
4848
limit: 1
49+
ha:
50+
interface: hacluster
51+
limit: 1
52+
optional: true
4953
peers:
5054
tls:
5155
interface: tls

poetry.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/abstract_charm.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def __init__(self, *args) -> None:
4848
self._database_requires = relations.database_requires.RelationEndpoint(self)
4949
self._database_provides = relations.database_provides.RelationEndpoint(self)
5050
self._cos_relation = relations.cos.COSRelation(self, self._container)
51+
self._ha_cluster = None
5152
self.framework.observe(self.on.update_status, self.reconcile)
5253
self.framework.observe(
5354
self.on[upgrade.PEER_RELATION_ENDPOINT_NAME].relation_changed, self.reconcile
@@ -212,6 +213,9 @@ def _determine_unit_status(self, *, event) -> ops.StatusBase:
212213
workload_status = self.get_workload(event=event).status
213214
if self._upgrade:
214215
statuses.append(self._upgrade.get_unit_juju_status(workload_status=workload_status))
216+
# only in machine charms
217+
if self._ha_cluster:
218+
statuses.append(self._ha_cluster.get_unit_juju_status())
215219
statuses.append(workload_status)
216220
return self._prioritize_statuses(statuses)
217221

@@ -311,6 +315,10 @@ def reconcile(self, event=None) -> None: # noqa: C901
311315
f"{self._cos_relation.is_relation_breaking(event)=}"
312316
)
313317

318+
# only in machine charms
319+
if self._ha_cluster:
320+
self._ha_cluster.set_vip(self.config.get("vip"))
321+
314322
try:
315323
if self._unit_lifecycle.authorized_leader:
316324
if self._database_requires.is_relation_breaking(event):
@@ -333,6 +341,14 @@ def reconcile(self, event=None) -> None: # noqa: C901
333341
exposed_read_only_endpoint=self._exposed_read_only_endpoint,
334342
shell=workload_.shell,
335343
)
344+
# _ha_cluster only assigned a value in machine charms
345+
if self._ha_cluster:
346+
self._database_provides.update_endpoints(
347+
router_read_write_endpoint=self._read_write_endpoint,
348+
router_read_only_endpoint=self._read_only_endpoint,
349+
exposed_read_write_endpoint=self._exposed_read_write_endpoint,
350+
exposed_read_only_endpoint=self._exposed_read_only_endpoint,
351+
)
336352
if workload_.container_ready:
337353
workload_.reconcile(
338354
event=event,

src/machine_charm.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import machine_upgrade
2121
import machine_workload
2222
import relations.database_providers_wrapper
23+
import relations.hacluster
2324
import snap
2425
import upgrade
2526
import workload
@@ -51,12 +52,14 @@ def __init__(self, *args) -> None:
5152
self, self._database_provides
5253
)
5354
self._authenticated_workload_type = machine_workload.AuthenticatedMachineWorkload
55+
self._ha_cluster = relations.hacluster.HACluster(self)
5456
self.framework.observe(self.on.install, self._on_install)
5557
self.framework.observe(self.on.remove, self._on_remove)
5658
self.framework.observe(self.on.upgrade_charm, self._on_upgrade_charm)
5759
self.framework.observe(
5860
self.on[machine_upgrade.FORCE_ACTION_NAME].action, self._on_force_upgrade_action
5961
)
62+
self.framework.observe(self.on.config_changed, self.reconcile)
6063

6164
@property
6265
def _subordinate_relation_endpoint_names(self) -> typing.Optional[typing.Iterable[str]]:
@@ -83,6 +86,12 @@ def _logrotate(self) -> machine_logrotate.LogRotate:
8386
@property
8487
def host_address(self) -> str:
8588
"""The host address for the machine."""
89+
if (
90+
self._ha_cluster.relation
91+
and self._ha_cluster.is_clustered()
92+
and self.config.get("vip")
93+
):
94+
return self.config["vip"]
8695
return str(self.model.get_binding("juju-info").network.bind_address)
8796

8897
@property

src/relations/database_providers_wrapper.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ def external_connectivity(self, event) -> bool:
4242
"""Whether any of the relations are marked as external."""
4343
return self._database_provides.external_connectivity(event)
4444

45+
def update_endpoints(
46+
self,
47+
*,
48+
router_read_write_endpoint: str,
49+
router_read_only_endpoint: str,
50+
exposed_read_write_endpoint: str,
51+
exposed_read_only_endpoint: str,
52+
) -> None:
53+
"""Update the endpoints in the provides relationship databags."""
54+
self._database_provides.update_endpoints(
55+
router_read_write_endpoint=router_read_write_endpoint,
56+
router_read_only_endpoint=router_read_only_endpoint,
57+
exposed_read_write_endpoint=exposed_read_write_endpoint,
58+
exposed_read_only_endpoint=exposed_read_only_endpoint,
59+
)
60+
4561
def reconcile_users(
4662
self,
4763
*,

src/relations/database_provides.py

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,20 @@ def __init__(self, *, app_name: str, endpoint_name: str) -> None:
4040
class _Relation:
4141
"""Relation to one application charm"""
4242

43-
def __init__(self, *, relation: ops.Relation) -> None:
43+
def __init__(
44+
self, *, relation: ops.Relation, interface: data_interfaces.DatabaseProvides
45+
) -> None:
4446
self._id = relation.id
4547

48+
# Application charm databag
49+
self._databag = remote_databag.RemoteDatabag(interface=interface, relation=relation)
50+
51+
# Whether endpoints should be externally accessible
52+
# (e.g. when related to `data-integrator` charm)
53+
# Implements DA073 - Add Expose Flag to the Database Interface
54+
# https://docs.google.com/document/d/1Y7OZWwMdvF8eEMuVKrqEfuFV3JOjpqLHL7_GPqJpRHU
55+
self.external_connectivity = self._databag.get("external-node-connectivity") == "true"
56+
4657
def __eq__(self, other) -> bool:
4758
if not isinstance(other, _Relation):
4859
return False
@@ -63,19 +74,12 @@ class _RelationThatRequestedUser(_Relation):
6374
def __init__(
6475
self, *, relation: ops.Relation, interface: data_interfaces.DatabaseProvides, event
6576
) -> None:
66-
super().__init__(relation=relation)
77+
super().__init__(relation=relation, interface=interface)
6778
self._interface = interface
6879
if isinstance(event, ops.RelationBrokenEvent) and event.relation.id == self._id:
6980
raise _RelationBreaking
70-
# Application charm databag
71-
databag = remote_databag.RemoteDatabag(interface=interface, relation=relation)
72-
self._database: str = databag["database"]
73-
# Whether endpoints should be externally accessible
74-
# (e.g. when related to `data-integrator` charm)
75-
# Implements DA073 - Add Expose Flag to the Database Interface
76-
# https://docs.google.com/document/d/1Y7OZWwMdvF8eEMuVKrqEfuFV3JOjpqLHL7_GPqJpRHU
77-
self.external_connectivity = databag.get("external-node-connectivity") == "true"
78-
if databag.get("extra-user-roles"):
81+
self._database: str = self._databag["database"]
82+
if self._databag.get("extra-user-roles"):
7983
raise _UnsupportedExtraUserRole(
8084
app_name=relation.app.name, endpoint_name=relation.name
8185
)
@@ -150,13 +154,40 @@ class _RelationWithSharedUser(_Relation):
150154
def __init__(
151155
self, *, relation: ops.Relation, interface: data_interfaces.DatabaseProvides
152156
) -> None:
153-
super().__init__(relation=relation)
157+
super().__init__(relation=relation, interface=interface)
154158
self._interface = interface
155159
self._local_databag = self._interface.fetch_my_relation_data([relation.id])[relation.id]
156160
for key in ("database", "username", "password", "endpoints", "read-only-endpoints"):
157161
if key not in self._local_databag:
158162
raise _UserNotShared
159163

164+
def update_endpoints(
165+
self,
166+
*,
167+
router_read_write_endpoint: str,
168+
router_read_only_endpoint: str,
169+
exposed_read_write_endpoint: str,
170+
exposed_read_only_endpoint: str,
171+
) -> None:
172+
"""Update the endpoints in the databag."""
173+
logger.debug(
174+
f"Updating endpoints {self._id} {router_read_write_endpoint=}, {router_read_only_endpoint=} {exposed_read_write_endpoint=} {exposed_read_only_endpoint=}"
175+
)
176+
rw_endpoint = (
177+
exposed_read_write_endpoint
178+
if self.external_connectivity
179+
else router_read_write_endpoint
180+
)
181+
ro_endpoint = (
182+
exposed_read_only_endpoint if self.external_connectivity else router_read_only_endpoint
183+
)
184+
185+
self._interface.set_endpoints(self._id, rw_endpoint)
186+
self._interface.set_read_only_endpoints(self._id, ro_endpoint)
187+
logger.debug(
188+
f"Updated endpoints {self._id} {router_read_write_endpoint=}, {router_read_only_endpoint=} {exposed_read_write_endpoint=} {exposed_read_only_endpoint=}"
189+
)
190+
160191
def delete_databag(self) -> None:
161192
"""Remove connection information from databag."""
162193
logger.debug(f"Deleting databag {self._id=}")
@@ -215,6 +246,23 @@ def external_connectivity(self, event) -> bool:
215246
pass
216247
return any(relation.external_connectivity for relation in requested_users)
217248

249+
def update_endpoints(
250+
self,
251+
*,
252+
router_read_write_endpoint: str,
253+
router_read_only_endpoint: str,
254+
exposed_read_write_endpoint: str,
255+
exposed_read_only_endpoint: str,
256+
) -> None:
257+
"""Update endpoints in the databags."""
258+
for relation in self._shared_users:
259+
relation.update_endpoints(
260+
router_read_write_endpoint=router_read_write_endpoint,
261+
router_read_only_endpoint=router_read_only_endpoint,
262+
exposed_read_write_endpoint=exposed_read_write_endpoint,
263+
exposed_read_only_endpoint=exposed_read_only_endpoint,
264+
)
265+
218266
def reconcile_users(
219267
self,
220268
*,

0 commit comments

Comments
 (0)