Skip to content

Commit

Permalink
[DPE-5249] Add integration support for hacluster (#177)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
shayancanonical authored Sep 23, 2024
1 parent 04c7b3a commit f9ea24c
Show file tree
Hide file tree
Showing 14 changed files with 654 additions and 40 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ jobs:
exclude:
- groups: {path_to_test_file: tests/integration/test_data_integrator.py}
ubuntu-versions: {series: focal}
- groups: {path_to_test_file: tests/integration/test_hacluster.py}
ubuntu-versions: {series: focal}
name: ${{ matrix.juju-snap-channel }} - (GH hosted) ${{ matrix.groups.job_name }} | ${{ matrix.ubuntu-versions.series }}
needs:
- lint
Expand Down
9 changes: 6 additions & 3 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ bases:
channel: "22.04"
architectures: [arm64]
parts:
files:
plugin: dump
source: .
prime:
- charm_version
- workload_version
charm:
override-pull: |
craftctl default
Expand All @@ -27,9 +33,6 @@ parts:
# TODO: enable after https://github.com/canonical/charmcraft/issues/1456 fixed
charm-strict-dependencies: false
charm-entrypoint: src/machine_charm.py
prime:
- charm_version
- workload_version
build-packages:
- libffi-dev
- libssl-dev
Expand Down
9 changes: 9 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

options:

vip:
description: |
Virtual IP to use to front mysql router units. Used only in case of external node connection.
type: string
19 changes: 14 additions & 5 deletions lib/charms/tempo_k8s/v2/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def __init__(self, *args):
)
from ops.framework import EventSource, Object
from ops.model import ModelError, Relation
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field

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

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

PYDEPS = ["pydantic"]

Expand Down Expand Up @@ -338,7 +338,7 @@ class Config:
class ProtocolType(BaseModel):
"""Protocol Type."""

model_config = ConfigDict(
model_config = ConfigDict( # type: ignore
# Allow serializing enum values.
use_enum_values=True
)
Expand Down Expand Up @@ -902,7 +902,16 @@ def _get_endpoint(
def get_endpoint(
self, protocol: ReceiverProtocol, relation: Optional[Relation] = None
) -> Optional[str]:
"""Receiver endpoint for the given protocol."""
"""Receiver endpoint for the given protocol.
It could happen that this function gets called before the provider publishes the endpoints.
In such a scenario, if a non-leader unit calls this function, a permission denied exception will be raised due to
restricted access. To prevent this, this function needs to be guarded by the `is_ready` check.
Raises:
ProtocolNotRequestedError:
If the charm unit is the leader unit and attempts to obtain an endpoint for a protocol it did not request.
"""
endpoint = self._get_endpoint(relation or self._relation, protocol=protocol)
if not endpoint:
requested_protocols = set()
Expand All @@ -925,7 +934,7 @@ def get_endpoint(
def charm_tracing_config(
endpoint_requirer: TracingEndpointRequirer, cert_path: Optional[Union[Path, str]]
) -> Tuple[Optional[str], Optional[str]]:
"""Utility function to determine the charm_tracing config you will likely want.
"""Return the charm_tracing config you likely want.
If no endpoint is provided:
disable charm tracing.
Expand Down
4 changes: 4 additions & 0 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ requires:
interface: tracing
optional: true
limit: 1
ha:
interface: hacluster
limit: 1
optional: true
peers:
tls:
interface: tls
Expand Down
1 change: 0 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions src/abstract_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def __init__(self, *args) -> None:
self._database_requires = relations.database_requires.RelationEndpoint(self)
self._database_provides = relations.database_provides.RelationEndpoint(self)
self._cos_relation = relations.cos.COSRelation(self, self._container)
self._ha_cluster = None
self.framework.observe(self.on.update_status, self.reconcile)
self.framework.observe(
self.on[upgrade.PEER_RELATION_ENDPOINT_NAME].relation_changed, self.reconcile
Expand Down Expand Up @@ -212,6 +213,9 @@ def _determine_unit_status(self, *, event) -> ops.StatusBase:
workload_status = self.get_workload(event=event).status
if self._upgrade:
statuses.append(self._upgrade.get_unit_juju_status(workload_status=workload_status))
# only in machine charms
if self._ha_cluster:
statuses.append(self._ha_cluster.get_unit_juju_status())
statuses.append(workload_status)
return self._prioritize_statuses(statuses)

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

# only in machine charms
if self._ha_cluster:
self._ha_cluster.set_vip(self.config.get("vip"))

try:
if self._unit_lifecycle.authorized_leader:
if self._database_requires.is_relation_breaking(event):
Expand All @@ -333,6 +341,14 @@ def reconcile(self, event=None) -> None: # noqa: C901
exposed_read_only_endpoint=self._exposed_read_only_endpoint,
shell=workload_.shell,
)
# _ha_cluster only assigned a value in machine charms
if self._ha_cluster:
self._database_provides.update_endpoints(
router_read_write_endpoint=self._read_write_endpoint,
router_read_only_endpoint=self._read_only_endpoint,
exposed_read_write_endpoint=self._exposed_read_write_endpoint,
exposed_read_only_endpoint=self._exposed_read_only_endpoint,
)
if workload_.container_ready:
workload_.reconcile(
event=event,
Expand Down
9 changes: 9 additions & 0 deletions src/machine_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import machine_upgrade
import machine_workload
import relations.database_providers_wrapper
import relations.hacluster
import snap
import upgrade
import workload
Expand Down Expand Up @@ -51,12 +52,14 @@ def __init__(self, *args) -> None:
self, self._database_provides
)
self._authenticated_workload_type = machine_workload.AuthenticatedMachineWorkload
self._ha_cluster = relations.hacluster.HACluster(self)
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.remove, self._on_remove)
self.framework.observe(self.on.upgrade_charm, self._on_upgrade_charm)
self.framework.observe(
self.on[machine_upgrade.FORCE_ACTION_NAME].action, self._on_force_upgrade_action
)
self.framework.observe(self.on.config_changed, self.reconcile)

@property
def _subordinate_relation_endpoint_names(self) -> typing.Optional[typing.Iterable[str]]:
Expand All @@ -83,6 +86,12 @@ def _logrotate(self) -> machine_logrotate.LogRotate:
@property
def host_address(self) -> str:
"""The host address for the machine."""
if (
self._ha_cluster.relation
and self._ha_cluster.is_clustered()
and self.config.get("vip")
):
return self.config["vip"]
return str(self.model.get_binding("juju-info").network.bind_address)

@property
Expand Down
16 changes: 16 additions & 0 deletions src/relations/database_providers_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ def external_connectivity(self, event) -> bool:
"""Whether any of the relations are marked as external."""
return self._database_provides.external_connectivity(event)

def update_endpoints(
self,
*,
router_read_write_endpoint: str,
router_read_only_endpoint: str,
exposed_read_write_endpoint: str,
exposed_read_only_endpoint: str,
) -> None:
"""Update the endpoints in the provides relationship databags."""
self._database_provides.update_endpoints(
router_read_write_endpoint=router_read_write_endpoint,
router_read_only_endpoint=router_read_only_endpoint,
exposed_read_write_endpoint=exposed_read_write_endpoint,
exposed_read_only_endpoint=exposed_read_only_endpoint,
)

def reconcile_users(
self,
*,
Expand Down
72 changes: 60 additions & 12 deletions src/relations/database_provides.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,20 @@ def __init__(self, *, app_name: str, endpoint_name: str) -> None:
class _Relation:
"""Relation to one application charm"""

def __init__(self, *, relation: ops.Relation) -> None:
def __init__(
self, *, relation: ops.Relation, interface: data_interfaces.DatabaseProvides
) -> None:
self._id = relation.id

# Application charm databag
self._databag = remote_databag.RemoteDatabag(interface=interface, relation=relation)

# Whether endpoints should be externally accessible
# (e.g. when related to `data-integrator` charm)
# Implements DA073 - Add Expose Flag to the Database Interface
# https://docs.google.com/document/d/1Y7OZWwMdvF8eEMuVKrqEfuFV3JOjpqLHL7_GPqJpRHU
self.external_connectivity = self._databag.get("external-node-connectivity") == "true"

def __eq__(self, other) -> bool:
if not isinstance(other, _Relation):
return False
Expand All @@ -63,19 +74,12 @@ class _RelationThatRequestedUser(_Relation):
def __init__(
self, *, relation: ops.Relation, interface: data_interfaces.DatabaseProvides, event
) -> None:
super().__init__(relation=relation)
super().__init__(relation=relation, interface=interface)
self._interface = interface
if isinstance(event, ops.RelationBrokenEvent) and event.relation.id == self._id:
raise _RelationBreaking
# Application charm databag
databag = remote_databag.RemoteDatabag(interface=interface, relation=relation)
self._database: str = databag["database"]
# Whether endpoints should be externally accessible
# (e.g. when related to `data-integrator` charm)
# Implements DA073 - Add Expose Flag to the Database Interface
# https://docs.google.com/document/d/1Y7OZWwMdvF8eEMuVKrqEfuFV3JOjpqLHL7_GPqJpRHU
self.external_connectivity = databag.get("external-node-connectivity") == "true"
if databag.get("extra-user-roles"):
self._database: str = self._databag["database"]
if self._databag.get("extra-user-roles"):
raise _UnsupportedExtraUserRole(
app_name=relation.app.name, endpoint_name=relation.name
)
Expand Down Expand Up @@ -150,13 +154,40 @@ class _RelationWithSharedUser(_Relation):
def __init__(
self, *, relation: ops.Relation, interface: data_interfaces.DatabaseProvides
) -> None:
super().__init__(relation=relation)
super().__init__(relation=relation, interface=interface)
self._interface = interface
self._local_databag = self._interface.fetch_my_relation_data([relation.id])[relation.id]
for key in ("database", "username", "password", "endpoints", "read-only-endpoints"):
if key not in self._local_databag:
raise _UserNotShared

def update_endpoints(
self,
*,
router_read_write_endpoint: str,
router_read_only_endpoint: str,
exposed_read_write_endpoint: str,
exposed_read_only_endpoint: str,
) -> None:
"""Update the endpoints in the databag."""
logger.debug(
f"Updating endpoints {self._id} {router_read_write_endpoint=}, {router_read_only_endpoint=} {exposed_read_write_endpoint=} {exposed_read_only_endpoint=}"
)
rw_endpoint = (
exposed_read_write_endpoint
if self.external_connectivity
else router_read_write_endpoint
)
ro_endpoint = (
exposed_read_only_endpoint if self.external_connectivity else router_read_only_endpoint
)

self._interface.set_endpoints(self._id, rw_endpoint)
self._interface.set_read_only_endpoints(self._id, ro_endpoint)
logger.debug(
f"Updated endpoints {self._id} {router_read_write_endpoint=}, {router_read_only_endpoint=} {exposed_read_write_endpoint=} {exposed_read_only_endpoint=}"
)

def delete_databag(self) -> None:
"""Remove connection information from databag."""
logger.debug(f"Deleting databag {self._id=}")
Expand Down Expand Up @@ -215,6 +246,23 @@ def external_connectivity(self, event) -> bool:
pass
return any(relation.external_connectivity for relation in requested_users)

def update_endpoints(
self,
*,
router_read_write_endpoint: str,
router_read_only_endpoint: str,
exposed_read_write_endpoint: str,
exposed_read_only_endpoint: str,
) -> None:
"""Update endpoints in the databags."""
for relation in self._shared_users:
relation.update_endpoints(
router_read_write_endpoint=router_read_write_endpoint,
router_read_only_endpoint=router_read_only_endpoint,
exposed_read_write_endpoint=exposed_read_write_endpoint,
exposed_read_only_endpoint=exposed_read_only_endpoint,
)

def reconcile_users(
self,
*,
Expand Down
Loading

0 comments on commit f9ea24c

Please sign in to comment.