Skip to content

[DPE-3689, DPE-4179] Expose read-write and read-only endpoints when related to data-integrator + TLS support #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0e93ef5
WIP: Expose read-write and read-only endpoints when related to data-i…
shayancanonical Mar 29, 2024
b4f85d0
Merge branch 'main' into feature/external_connectivity
shayancanonical Apr 4, 2024
ad8b129
A working version of charm with simultaneous relations with COS and TLS
shayancanonical Apr 9, 2024
6dae91c
Avoid using kw_only in dataclasses due to python3.8 in juju 3.1.7
shayancanonical Apr 10, 2024
716da08
Add TLS integration test + test COS when related to TLS operator
shayancanonical Apr 11, 2024
8293c94
Add integration test for data integrator and tls operator; for extern…
shayancanonical Apr 11, 2024
5b3627b
Skip data integrator tests on focal
shayancanonical Apr 11, 2024
56c0c94
Run format
shayancanonical Apr 11, 2024
61e2c8c
Use latest/stable for data integrator
shayancanonical Apr 11, 2024
6b47d09
Avoid running data integration tests when focal only
shayancanonical Apr 12, 2024
eddf296
Attempt at correcting ci.yaml invalid workflow file
shayancanonical Apr 12, 2024
5fa3bc7
Use matrix exclude instead of if conditions
shayancanonical Apr 12, 2024
26235fd
Remove quotes from exclusion group
shayancanonical Apr 12, 2024
3657e83
Another attempt at matrix exclusion
shayancanonical Apr 12, 2024
b7afd29
Use juju_.has_secrets to be correctly handle action result return cod…
shayancanonical Apr 12, 2024
d9c072c
Fix mixup in return code keys across juju versions
shayancanonical Apr 12, 2024
79fd8ef
Fix typo
shayancanonical Apr 12, 2024
ee4b098
Address PR feedback
shayancanonical Apr 17, 2024
6f4b271
Add missing monkeypatch method for unit tests
shayancanonical Apr 17, 2024
eba3c8f
Avoid re-bootstrapping in workload's reconcile method
shayancanonical Apr 18, 2024
2e276ba
Update charmed-mysql snap revision to latest 8.0/edge revision
shayancanonical Apr 18, 2024
0cf8b71
Address PR feedback
shayancanonical Apr 19, 2024
0a36768
Extend wait time in exporter tests + revert ops dependency to <2.10.0
shayancanonical Apr 22, 2024
1134e43
Use tenacity to retry checking exporter endpoints instead of using ti…
shayancanonical Apr 22, 2024
41d7616
Run format
shayancanonical Apr 22, 2024
6f60488
Update data_interfaces charm lib to v0.34
shayancanonical Apr 22, 2024
40beb97
Miscellaneous integration test improvements
shayancanonical Apr 22, 2024
1b0bb19
Address PR feedback + fix broken upgrades
shayancanonical Apr 24, 2024
996ba1e
Merge branch 'main' into feature/external_connectivity
shayancanonical Apr 24, 2024
b99ecce
Merge branch 'main' into feature/external_connectivity
shayancanonical Apr 24, 2024
10748cf
Address PR feedback
shayancanonical Apr 24, 2024
03d4ed4
Fix assertion message in integration test
shayancanonical Apr 24, 2024
0978187
Address PR feedback
shayancanonical Apr 24, 2024
f8828dc
Fix bugs introduced while addressing PR feedback; pass event as kwarg
shayancanonical Apr 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
resume-upgrade:
description: Upgrade remaining units (after you manually verified that upgraded units are healthy).
force-upgrade:
description: Force upgrade of this unit.
description: |
Potential of *data loss* and *downtime*

Force upgrade of this unit.

Use to
- force incompatible upgrade and/or
- continue upgrade if 1+ upgraded units have non-active status
set-tls-private-key:
description:
Set the private key, which will be used for certificate signing requests (CSR). Run
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

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

6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ authors = []

[tool.poetry.dependencies]
python = "^3.8.1" # ^3.8.1 required by flake8
# there is a breaking change in ops 2.10.0: https://github.com/canonical/operator/pull/1091#issuecomment-1888644075
ops = "<2.10.0"
tenacity = "^8.2.3"
poetry-core = "^1.7.0"
Expand All @@ -18,6 +19,7 @@ requests = "^2.31.0"

[tool.poetry.group.charm-libs.dependencies]
# data_platform_libs/v0/data_interfaces.py
# there is a breaking change in ops 2.10.0: https://github.com/canonical/operator/pull/1091#issuecomment-1888644075
ops = "<2.10.0"
# tls_certificates_interface/v2/tls_certificates.py
# tls_certificates lib v2 uses a feature only available in cryptography >=42.0.5
Expand Down Expand Up @@ -53,7 +55,7 @@ pytest = "^7.4.0"
pytest-xdist = "^3.3.1"
pytest-cov = "^4.1.0"
ops-scenario = "^5.4.1"
ops = "<2.10.0"
ops = ">=2.0.0"
pytest-mock = "^3.11.1"

[tool.poetry.group.integration.dependencies]
Expand All @@ -65,7 +67,7 @@ pytest-github-secrets = {git = "https://github.com/canonical/data-platform-workf
juju = "3.2.0.1"
mysql-connector-python = "~8.0.33"
tenacity = "^8.2.2"
ops = "<2.10.0"
ops = ">=2.0.0"
pytest-mock = "^3.11.1"


Expand Down
9 changes: 5 additions & 4 deletions src/abstract_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,9 @@ def _exposed_read_write_endpoint(self) -> str:
def _exposed_read_only_endpoint(self) -> str:
"""The exposed read-only endpoint"""

@property
@abc.abstractmethod
def is_exposed(self) -> typing.Optional[bool]:
"""Whether router is exposed externally"""
def is_externally_accessible(self, event=None) -> typing.Optional[bool]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please remove default None since event should always be passed (even if in reconcile, the value of event is none because it's not a relation-breaking event, that value should always be passed here—this function should never be called without an event arg I believe)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 0978187

"""Whether router is externally accessible"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is this only used by vm charm?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it is only used by the vm charm. but needs to be defined in abstract_charm because reconcile in workload calls to it. in vm, it will return True/False, in k8s it will return None

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you mention that it's only used by machine charm in docstring?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 0978187


@property
def _tls_certificate_saved(self) -> bool:
Expand Down Expand Up @@ -269,7 +268,9 @@ def reconcile(self, event=None) -> None: # noqa: C901
if self._upgrade.unit_state == "outdated":
if self._upgrade.authorized:
self._upgrade.upgrade_unit(
workload_=workload_, tls=self._tls_certificate_saved
workload_=workload_,
tls=self._tls_certificate_saved,
exporter_config=self._cos_exporter_config(event),
)
else:
self.set_status(event=event)
Expand Down
9 changes: 4 additions & 5 deletions src/machine_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,15 @@ def _exposed_read_write_endpoint(self) -> str:
def _exposed_read_only_endpoint(self) -> str:
return f"{self.host_address}:{self._READ_ONLY_PORT}"

@property
def is_exposed(self) -> typing.Optional[bool]:
return self._database_provides.external_connectivity
def is_externally_accessible(self, event=None) -> typing.Optional[bool]:
return self._database_provides.external_connectivity(event)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here with event should always be passed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 0978187


def _reconcile_node_port(self, event) -> None:
"""Only applies to Kubernetes charm, so no-op."""
pass

def _reconcile_ports(self) -> None:
if self.is_exposed:
if self.is_externally_accessible():
ports = [self._READ_WRITE_PORT, self._READ_ONLY_PORT]
else:
ports = []
Expand All @@ -108,7 +107,7 @@ def wait_until_mysql_router_ready(self) -> None:
wait=tenacity.wait_fixed(5),
):
with attempt:
if self.is_exposed:
if self.is_externally_accessible():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does event need to be passed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 0978187

for port in (
self._READ_WRITE_PORT,
self._READ_ONLY_PORT,
Expand Down
6 changes: 4 additions & 2 deletions src/machine_logrotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def enable(self) -> None:

def disable(self) -> None:
logger.debug("Removing cron job for log rotation of mysqlrouter")
self._logrotate_config.unlink()
self._cron_file.unlink()
if self._logrotate_config.exists():
self._logrotate_config.unlink()
if self._cron_file.exists():
self._cron_file.unlink()
logger.debug("Removed cron job for log rotation of mysqlrouter")
13 changes: 11 additions & 2 deletions src/machine_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import upgrade
import workload

if typing.TYPE_CHECKING:
import relations.cos

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -152,10 +155,16 @@ def authorized(self) -> bool:
return False
return False

def upgrade_unit(self, *, workload_: workload.Workload, tls: bool) -> None:
def upgrade_unit(
self,
*,
workload_: workload.Workload,
tls: bool,
exporter_config: "relations.cos.ExporterConfig",
) -> None:
logger.debug(f"Upgrading {self.authorized=}")
self.unit_state = "upgrading"
workload_.upgrade(unit=self._unit, tls=tls)
workload_.upgrade(unit=self._unit, tls=tls, exporter_config=exporter_config)
self._unit_workload_container_version = snap.REVISION
self._unit_workload_version = self._current_versions["workload"]
logger.debug(
Expand Down
13 changes: 6 additions & 7 deletions src/machine_workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class AuthenticatedMachineWorkload(workload.AuthenticatedWorkload):
# TODO python3.10 min version: Use `list` instead of `typing.List`
def _get_bootstrap_command(self, password: str) -> typing.List[str]:
command = super()._get_bootstrap_command(password)
if self._charm.is_exposed:
if self._charm.is_externally_accessible():
command.extend(
[
"--conf-bind-address",
Expand All @@ -35,17 +35,18 @@ def _get_bootstrap_command(self, password: str) -> typing.List[str]:
# set. Workaround for https://bugs.mysql.com/bug.php?id=107291
"--conf-set-option",
"DEFAULT.server_ssl_mode=PREFERRED",
"--conf-skip-tcp",
]
)
return command

def _update_configured_socket_file_locations_and_bind_address(self) -> None:
def _update_configured_socket_file_locations(self) -> None:
"""Update configured socket file locations from `/tmp` to `/run/mysqlrouter`.

Called after MySQL Router bootstrap & before MySQL Router service is enabled

Change configured location of socket files before socket files are created by MySQL Router
service. Also remove bind_address and bind_port for all router services: rw, ro, x_rw, x_ro
service.

Needed since `/tmp` inside a snap is not accessible to non-root users. The socket files
must be accessible to applications related via database_provides endpoint.
Expand All @@ -59,14 +60,12 @@ def _update_configured_socket_file_locations_and_bind_address(self) -> None:
section["socket"] = str(
self._container.path("/run/mysqlrouter") / pathlib.PurePath(section["socket"]).name
)
del section["bind_address"]
del section["bind_port"]
with io.StringIO() as output:
config.write(output)
self._container.router_config_file.write_text(output.getvalue())
logger.debug("Updated configured socket file locations")

def _bootstrap_router(self, *, tls: bool) -> None:
super()._bootstrap_router(tls=tls)
if not self._charm.is_exposed:
self._update_configured_socket_file_locations_and_bind_address()
if not self._charm.is_externally_accessible():
self._update_configured_socket_file_locations()
7 changes: 3 additions & 4 deletions src/relations/database_providers_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ def __init__(
charm_
)

@property
def external_connectivity(self) -> bool:
"""Whether the relation is exposed"""
return self._database_provides.external_connectivity
def external_connectivity(self, event) -> bool:
"""Whether any of the relations are marked as external."""
return self._database_provides.external_connectivity(event)

def reconcile_users(
self,
Expand Down
32 changes: 20 additions & 12 deletions src/relations/database_provides.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def __init__(
# (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"
self.external_connectivity = databag.get("external-node-connectivity") == "true"
if databag.get("extra-user-roles"):
raise _UnsupportedExtraUserRole(
app_name=relation.app.name, endpoint_name=relation.name
Expand Down Expand Up @@ -125,13 +125,11 @@ def create_database_and_user(

rw_endpoint = (
exposed_read_write_endpoint
if self._external_connectivity
if self.external_connectivity
else router_read_write_endpoint
)
ro_endpoint = (
exposed_read_only_endpoint
if self._external_connectivity
else router_read_only_endpoint
exposed_read_only_endpoint if self.external_connectivity else router_read_only_endpoint
)

self._set_databag(
Expand Down Expand Up @@ -199,13 +197,23 @@ def _shared_users(self) -> typing.List[_RelationWithSharedUser]:
pass
return shared_users

@property
def external_connectivity(self) -> bool:
"""Whether the relation is exposed."""
relation_data = self._interface.fetch_relation_data(fields=["external-node-connectivity"])
return any(
[data.get("external-node-connectivity") == "true" for data in relation_data.values()]
)
def external_connectivity(self, event) -> bool:
"""Whether any of the relations are marked as external."""
requested_users = []
for relation in self._interface.relations:
try:
requested_users.append(
_RelationThatRequestedUser(
relation=relation, interface=self._interface, event=event
)
)
except (
_RelationBreaking,
remote_databag.IncompleteDatabag,
_UnsupportedExtraUserRole,
):
pass
return any(relation.external_connectivity for relation in requested_users)

def reconcile_users(
self,
Expand Down
2 changes: 1 addition & 1 deletion src/relations/tls.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this file shared across vm & k8s?

if not, can it be? it appears to be nearly identical

IMO, we should get back to the pattern that if the file name is the same on the vm and k8s charm, the file is identical on both charms. Otherwise, behavior across vm & k8s router will diverge & the maintenance cost will increase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree with you, but imo, there's some divergence between the vm and k8s router charms. i would be an advocate of separating shared files and converging the two charms as a separate effort that fast follows up this PR

Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def save_certificate(self, event: tls_certificates.CertificateAvailableEvent) ->
def _generate_csr(self, key: bytes) -> bytes:
"""Generate certificate signing request (CSR)."""
sans_ip = ["127.0.0.1"] # needed for the HTTP server when related with COS
if self._charm.is_exposed:
if self._charm.is_externally_accessible():
sans_ip.append(self._charm.host_address)

return tls_certificates.generate_csr(
Expand Down
11 changes: 11 additions & 0 deletions src/snap.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,24 @@ def update_mysql_router_exporter_service(
"mysqlrouter-exporter.service-name": self._unit_name.replace("/", "-"),
}
)
if tls:
_snap.set(
{
"mysqlrouter.tls-cacert-path": certificate_authority_filename,
"mysqlrouter.tls-cert-path": certificate_filename,
"mysqlrouter.tls-key-path": key_filename,
}
)
_snap.start([self._EXPORTER_SERVICE_NAME], enable=True)
else:
_snap.stop([self._EXPORTER_SERVICE_NAME], disable=True)
_snap.unset("mysqlrouter-exporter.user")
_snap.unset("mysqlrouter-exporter.password")
_snap.unset("mysqlrouter-exporter.url")
_snap.unset("mysqlrouter-exporter.service-name")
_snap.unset("mysqlrouter.tls-cacert-path")
_snap.unset("mysqlrouter.tls-cert-path")
_snap.unset("mysqlrouter.tls-key-path")

def upgrade(self, unit: ops.Unit) -> None:
"""Upgrade snap."""
Expand Down
Loading