Skip to content

Commit ee4b098

Browse files
Address PR feedback
1 parent 79fd8ef commit ee4b098

File tree

13 files changed

+168
-88
lines changed

13 files changed

+168
-88
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,7 @@ jobs:
159159
echo "mark_expression=not unstable" >> "$GITHUB_OUTPUT"
160160
fi
161161
- name: Run integration tests
162-
if:
163-
run:
164-
tox run -e integration -- "${{ matrix.groups.path_to_test_file }}" --group="${{ matrix.groups.group_number }}" -m '${{ steps.select-test-stability.outputs.mark_expression }}' --mysql-router-charm-series=${{ matrix.ubuntu-versions.series }} --mysql-router-charm-bases-index=${{ matrix.ubuntu-versions.bases-index }}
162+
run: tox run -e integration -- "${{ matrix.groups.path_to_test_file }}" --group="${{ matrix.groups.group_number }}" -m '${{ steps.select-test-stability.outputs.mark_expression }}' --mysql-router-charm-series=${{ matrix.ubuntu-versions.series }} --mysql-router-charm-bases-index=${{ matrix.ubuntu-versions.bases-index }}
165163
env:
166164
LIBJUJU_VERSION_SPECIFIER: ${{ matrix.libjuju-version }}
167165
SECRETS_FROM_GITHUB: |

poetry.lock

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

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ authors = []
1010

1111
[tool.poetry.dependencies]
1212
python = "^3.8.1" # ^3.8.1 required by flake8
13-
ops = "^2.0.0"
13+
ops = "^2.8.0"
1414
tenacity = "^8.2.3"
1515
poetry-core = "^1.7.0"
1616
jinja2 = "^3.1.2"
1717
requests = "^2.31.0"
1818

1919
[tool.poetry.group.charm-libs.dependencies]
2020
# data_platform_libs/v0/data_interfaces.py
21-
ops = ">=2.0.0"
21+
ops = ">=2.8.0"
2222
# tls_certificates_interface/v1/tls_certificates.py
2323
cryptography = ">=42.0.5"
2424
jsonschema = "*"
@@ -52,7 +52,7 @@ pytest = "^7.4.0"
5252
pytest-xdist = "^3.3.1"
5353
pytest-cov = "^4.1.0"
5454
ops-scenario = "^5.4.1"
55-
ops = ">=2.0.0"
55+
ops = ">=2.8.0"
5656
pytest-mock = "^3.11.1"
5757

5858
[tool.poetry.group.integration.dependencies]
@@ -64,7 +64,7 @@ pytest-github-secrets = {git = "https://github.com/canonical/data-platform-workf
6464
juju = "3.2.0.1"
6565
mysql-connector-python = "~8.0.33"
6666
tenacity = "^8.2.2"
67-
ops = ">=2.0.0"
67+
ops = ">=2.8.0"
6868
pytest-mock = "^3.11.1"
6969

7070

src/abstract_charm.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
class MySQLRouterCharm(ops.CharmBase, abc.ABC):
3030
"""MySQL Router charm"""
3131

32+
_READ_WRITE_PORT = 6446
33+
_READ_ONLY_PORT = 6447
34+
3235
def __init__(self, *args) -> None:
3336
super().__init__(*args)
3437
# Instantiate before registering other event observers
@@ -96,6 +99,16 @@ def _read_write_endpoint(self) -> str:
9699
def _read_only_endpoint(self) -> str:
97100
"""MySQL Router read-only endpoint"""
98101

102+
@property
103+
@abc.abstractmethod
104+
def _exposed_read_write_endpoint(self) -> str:
105+
"""The exposed read-write endpoint"""
106+
107+
@property
108+
@abc.abstractmethod
109+
def _exposed_read_only_endpoint(self) -> str:
110+
"""The exposed read-only endpoint"""
111+
99112
@property
100113
@abc.abstractmethod
101114
def _tls_certificate_saved(self) -> bool:
@@ -116,6 +129,11 @@ def _tls_certificate_authority(self) -> typing.Optional[str]:
116129
def _tls_certificate(self) -> typing.Optional[str]:
117130
"""Custom TLS certificate"""
118131

132+
@property
133+
@abc.abstractmethod
134+
def _substrate(self) -> str:
135+
"""Returns the substrate of the charm: vm or k8s"""
136+
119137
@abc.abstractmethod
120138
def is_exposed(self, relation=None) -> bool:
121139
"""Whether router is exposed externally"""
@@ -208,19 +226,38 @@ def wait_until_mysql_router_ready(self) -> None:
208226
wait=tenacity.wait_fixed(5),
209227
):
210228
with attempt:
211-
if self.is_exposed():
229+
if self._substrate == "k8s" or self.is_exposed():
212230
for port in (6446, 6447):
213231
with socket.socket() as s:
214232
assert s.connect_ex(("localhost", port)) == 0
215233
else:
216-
assert self._container.path("/run/mysqlrouter/mysql.sock").exists()
217-
assert self._container.path("/run/mysqlrouter/mysqlro.sock").exists()
234+
for socket_file in (
235+
"/run/mysqlrouter/mysql.sock",
236+
"/run/mysqlrouter/mysqlro.sock",
237+
):
238+
assert self._container.path(socket_file).exists()
239+
with socket.socket(socket.AF_UNIX) as s:
240+
assert s.connect_ex(str(self._container.path(socket_file))) == 0
218241
except AssertionError:
219242
logger.exception("Unable to connect to MySQL Router")
220243
raise
221244
else:
222245
logger.debug("MySQL Router is ready")
223246

247+
@abc.abstractmethod
248+
def _reconcile_node_port(self, event) -> None:
249+
"""Reconcile node port.
250+
251+
Only applies to Kubernetes charm
252+
"""
253+
254+
@abc.abstractmethod
255+
def _reconcile_ports(self) -> None:
256+
"""Reconcile exposed ports.
257+
258+
Only applies to Machine charm
259+
"""
260+
224261
# =======================
225262
# Handlers
226263
# =======================
@@ -286,21 +323,29 @@ def reconcile(self, event=None) -> None: # noqa: C901
286323
and isinstance(workload_, workload.AuthenticatedWorkload)
287324
and workload_.container_ready
288325
):
326+
self._reconcile_node_port(event=event)
289327
self._database_provides.reconcile_users(
290328
event=event,
291329
router_read_write_endpoint=self._read_write_endpoint,
292330
router_read_only_endpoint=self._read_only_endpoint,
331+
exposed_read_write_endpoint=self._exposed_read_write_endpoint,
332+
exposed_read_only_endpoint=self._exposed_read_only_endpoint,
293333
shell=workload_.shell,
294334
)
295335
if workload_.container_ready:
296336
workload_.reconcile(
297-
tls=self.tls.relation_exists and not self.tls.is_relation_breaking(event),
337+
tls=self._tls_certificate_saved,
298338
unit_name=self.unit.name,
299339
exporter_config=self._cos_exporter_config(event),
300340
key=self._tls_key,
301341
certificate=self._tls_certificate,
302342
certificate_authority=self._tls_certificate_authority,
303343
)
344+
if not self._upgrade.in_progress and isinstance(
345+
workload_, workload.AuthenticatedWorkload
346+
):
347+
self._reconcile_ports()
348+
304349
# Empty waiting status means we're waiting for database requires relation before
305350
# starting workload
306351
if not workload_.status or workload_.status == ops.WaitingStatus():

src/container.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def __init__(
103103
self._mysql_router_command = mysql_router_command
104104
self._mysql_shell_command = mysql_shell_command
105105
self._mysql_router_password_command = mysql_router_password_command
106-
self.unit_name = unit_name
106+
self._unit_name = unit_name
107107

108108
@property
109109
@abc.abstractmethod

src/machine_charm.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@
2626
class MachineSubordinateRouterCharm(abstract_charm.MySQLRouterCharm):
2727
"""MySQL Router machine subordinate charm"""
2828

29-
READ_WRITE_PORT = 6446
30-
READ_ONLY_PORT = 6447
31-
3229
def __init__(self, *args) -> None:
3330
super().__init__(*args)
3431
# DEPRECATED shared-db: Enable legacy "mysql-shared" interface
@@ -64,22 +61,26 @@ def _logrotate(self) -> machine_logrotate.LogRotate:
6461
return machine_logrotate.LogRotate(container_=self._container)
6562

6663
@property
67-
def _host_address(self) -> str:
64+
def host_address(self) -> str:
6865
"""The host address for the machine."""
69-
return self.model.get_binding("juju-info").network.bind_address
66+
return str(self.model.get_binding("juju-info").network.bind_address)
7067

7168
@property
7269
def _read_write_endpoint(self) -> str:
73-
if self.is_exposed():
74-
return f"{self._host_address}:{self.READ_WRITE_PORT}"
7570
return f'file://{self._container.path("/run/mysqlrouter/mysql.sock")}'
7671

7772
@property
7873
def _read_only_endpoint(self) -> str:
79-
if self.is_exposed():
80-
return f"{self._host_address}:{self.READ_ONLY_PORT}"
8174
return f'file://{self._container.path("/run/mysqlrouter/mysqlro.sock")}'
8275

76+
@property
77+
def _exposed_read_write_endpoint(self) -> str:
78+
return f"{self.host_address}:{self._READ_WRITE_PORT}"
79+
80+
@property
81+
def _exposed_read_only_endpoint(self) -> str:
82+
return f"{self.host_address}:{self._READ_ONLY_PORT}"
83+
8384
@property
8485
def _tls_certificate_saved(self) -> bool:
8586
"""Whether a TLS certificate is available to use"""
@@ -102,6 +103,21 @@ def _tls_certificate_authority(self) -> typing.Optional[str]:
102103
def is_exposed(self, relation=None) -> bool:
103104
return self._database_provides.is_exposed
104105

106+
def _reconcile_node_port(self, event) -> None:
107+
"""Only applies to Kubernetes charm, so no-op."""
108+
pass
109+
110+
def _reconcile_ports(self) -> None:
111+
if self.is_exposed():
112+
ports = [self._READ_WRITE_PORT, self._READ_ONLY_PORT]
113+
else:
114+
ports = []
115+
self.unit.set_ports(*ports)
116+
117+
@property
118+
def _substrate(self) -> str:
119+
return "vm"
120+
105121
# =======================
106122
# Handlers
107123
# =======================
@@ -146,10 +162,6 @@ def _on_force_upgrade_action(self, event: ops.ActionEvent) -> None:
146162
event.set_results({"result": f"Forcefully upgraded {self.unit.name}"})
147163
logger.debug("Forced upgrade")
148164

149-
def reconcile(self, event=None) -> None:
150-
self._database_provides.reconcile_ports()
151-
super().reconcile(event=event)
152-
153165

154166
if __name__ == "__main__":
155167
ops.main.main(MachineSubordinateRouterCharm)

src/relations/database_providers_wrapper.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,14 @@ def is_exposed(self) -> bool:
4343
"""Whether the relation is exposed"""
4444
return self._database_provides.is_exposed
4545

46-
def reconcile_ports(self) -> None:
47-
"""Reconcile ports for this unit"""
48-
self._database_provides.reconcile_ports()
49-
5046
def reconcile_users(
5147
self,
5248
*,
5349
event,
5450
router_read_write_endpoint: str,
5551
router_read_only_endpoint: str,
52+
exposed_read_write_endpoint: str,
53+
exposed_read_only_endpoint: str,
5654
shell: mysql_shell.Shell,
5755
) -> None:
5856
"""Create requested users and delete inactive users.
@@ -65,6 +63,8 @@ def reconcile_users(
6563
event=event,
6664
router_read_write_endpoint=router_read_write_endpoint,
6765
router_read_only_endpoint=router_read_only_endpoint,
66+
exposed_read_write_endpoint=exposed_read_write_endpoint,
67+
exposed_read_only_endpoint=exposed_read_only_endpoint,
6868
shell=shell,
6969
)
7070
self._deprecated_shared_db.reconcile_users(event=event, shell=shell)

src/relations/database_provides.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ def __init__(
7070
# Application charm databag
7171
databag = remote_databag.RemoteDatabag(interface=interface, relation=relation)
7272
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"
7378
if databag.get("extra-user-roles"):
7479
raise _UnsupportedExtraUserRole(
7580
app_name=relation.app.name, endpoint_name=relation.name
@@ -100,6 +105,8 @@ def create_database_and_user(
100105
*,
101106
router_read_write_endpoint: str,
102107
router_read_only_endpoint: str,
108+
exposed_read_write_endpoint: str,
109+
exposed_read_only_endpoint: str,
103110
shell: mysql_shell.Shell,
104111
) -> None:
105112
"""Create database & user and update databag."""
@@ -115,11 +122,23 @@ def create_database_and_user(
115122
password = shell.create_application_database_and_user(
116123
username=username, database=self._database
117124
)
125+
126+
rw_endpoint = (
127+
exposed_read_write_endpoint
128+
if self._external_connectivity
129+
else router_read_write_endpoint
130+
)
131+
ro_endpoint = (
132+
exposed_read_only_endpoint
133+
if self._external_connectivity
134+
else router_read_only_endpoint
135+
)
136+
118137
self._set_databag(
119138
username=username,
120139
password=password,
121-
router_read_write_endpoint=router_read_write_endpoint,
122-
router_read_only_endpoint=router_read_only_endpoint,
140+
router_read_write_endpoint=rw_endpoint,
141+
router_read_only_endpoint=ro_endpoint,
123142
)
124143

125144

@@ -189,21 +208,14 @@ def is_exposed(self) -> bool:
189208
[data.get("external-node-connectivity") == "true" for data in relation_data.values()]
190209
)
191210

192-
def reconcile_ports(self) -> None:
193-
"""Reconcile ports for this unit"""
194-
if self.is_exposed:
195-
self._charm.unit.open_port("tcp", self._charm.READ_WRITE_PORT)
196-
self._charm.unit.open_port("tcp", self._charm.READ_ONLY_PORT)
197-
else:
198-
self._charm.unit.close_port("tcp", self._charm.READ_WRITE_PORT)
199-
self._charm.unit.close_port("tcp", self._charm.READ_ONLY_PORT)
200-
201211
def reconcile_users(
202212
self,
203213
*,
204214
event,
205215
router_read_write_endpoint: str,
206216
router_read_only_endpoint: str,
217+
exposed_read_write_endpoint: str,
218+
exposed_read_only_endpoint: str,
207219
shell: mysql_shell.Shell,
208220
) -> None:
209221
"""Create requested users and delete inactive users.
@@ -235,6 +247,8 @@ def reconcile_users(
235247
relation.create_database_and_user(
236248
router_read_write_endpoint=router_read_write_endpoint,
237249
router_read_only_endpoint=router_read_only_endpoint,
250+
exposed_read_write_endpoint=exposed_read_write_endpoint,
251+
exposed_read_only_endpoint=exposed_read_only_endpoint,
238252
shell=shell,
239253
)
240254
for relation in self._shared_users:

0 commit comments

Comments
 (0)