Skip to content
This repository has been archived by the owner on Jun 7, 2024. It is now read-only.

Commit

Permalink
Refactor the code
Browse files Browse the repository at this point in the history
  • Loading branch information
weiiwang01 committed Aug 8, 2023
1 parent f123133 commit 722c7b7
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 156 deletions.
39 changes: 23 additions & 16 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from constants import FLASK_CONTAINER_NAME
from databases import Databases
from exceptions import CharmConfigInvalidError, PebbleNotReadyError
from flask_app import FlaskApp
from flask_app import restart_flask
from observability import Observability
from secret_storage import SecretStorage
from webserver import GunicornWebserver
Expand Down Expand Up @@ -45,10 +45,9 @@ def __init__(self, *args: typing.Any) -> None:
charm_state=self._charm_state,
flask_container=self.unit.get_container(FLASK_CONTAINER_NAME),
)
self._flask_app = FlaskApp(
self._databases = Databases(
charm=self, charm_state=self._charm_state, webserver=self._webserver
)
self._databases = Databases(charm=self, flask_app=self._flask_app)
self._charm_state.database_uris = self._databases.get_uris()
self._ingress = IngressPerAppRequirer(
self,
Expand All @@ -71,16 +70,6 @@ def __init__(self, *args: typing.Any) -> None:
self.on.secret_storage_relation_changed, self._on_secret_storage_relation_changed
)

def _update_app_and_unit_status(self, status: ops.StatusBase) -> None:
"""Update the application and unit status.
Args:
status: the desired application and unit status.
"""
self.unit.status = status
if self.unit.is_leader():
self.app.status = status

def container(self) -> ops.Container:
"""Get the flask application workload container controller.
Expand All @@ -103,7 +92,7 @@ def _on_config_changed(self, _event: ops.EventBase) -> None:
Args:
_event: the config-changed event that triggers this callback function.
"""
self._flask_app.restart_flask()
self._restart_flask()

def _on_statsd_prometheus_exporter_pebble_ready(self, _event: ops.PebbleReadyEvent) -> None:
"""Handle the statsd-prometheus-exporter-pebble-ready event."""
Expand Down Expand Up @@ -145,15 +134,33 @@ def _on_rotate_secret_key_action(self, event: ops.ActionEvent) -> None:
return
self._secret_storage.reset_flask_secret_key()
event.set_results({"status": "success"})
self._flask_app.restart_flask()
self._restart_flask()

def _on_secret_storage_relation_changed(self, _event: ops.RelationEvent) -> None:
"""Handle the secret-storage-relation-changed event.
Args:
_event: the action event that triggers this callback.
"""
self._flask_app.restart_flask()
self._restart_flask()

def _update_app_and_unit_status(self, status: ops.StatusBase) -> None:
"""Update the application and unit status.
Args:
status: the desired application and unit status.
"""
self.unit.status = status
if self.unit.is_leader():
self.app.status = status

def _restart_flask(self) -> None:
"""Restart or start the flask service if not started with the latest configuration."""
try:
restart_flask(charm=self, charm_state=self._charm_state, webserver=self._webserver)
self._update_app_and_unit_status(ops.ActiveStatus())
except CharmConfigInvalidError as exc:
self._update_app_and_unit_status(ops.BlockedStatus(exc.msg))


if __name__ == "__main__": # pragma: nocover
Expand Down
39 changes: 33 additions & 6 deletions src/databases.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
import yaml
from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires, DatabaseRequiresEvent

from charm_state import CharmState
from constants import FLASK_DATABASE_NAME, FLASK_SUPPORTED_DB_INTERFACES
from flask_app import FlaskApp
from exceptions import CharmConfigInvalidError
from flask_app import restart_flask
from webserver import GunicornWebserver

logger = logging.getLogger(__name__)

Expand All @@ -27,17 +30,21 @@ class Databases(ops.Object): # pylint: disable=too-few-public-methods
_databases: A dict of DatabaseRequires to store relations
"""

def __init__(self, charm: ops.CharmBase, flask_app: FlaskApp):
def __init__(
self, charm: ops.CharmBase, charm_state: CharmState, webserver: GunicornWebserver
):
"""Initialize a new instance of the Databases class.
Args:
charm: The main charm. Used for events callbacks
flask_app: The flask application manager
charm: The main charm. Used for events callbacks.
charm_state: The charm's state.
webserver: The webserver manager object.
"""
# The following is necessary to be able to subscribe to callbacks from ops.framework
super().__init__(charm, "databases")
self._charm = charm
self._flask_app = flask_app
self._charm_state = charm_state
self._webserver = webserver

metadata = yaml.safe_load(pathlib.Path("metadata.yaml").read_text(encoding="utf-8"))
self._db_interfaces = (
Expand All @@ -53,13 +60,33 @@ def __init__(self, charm: ops.CharmBase, flask_app: FlaskApp):
for name in self._db_interfaces
}

def _update_app_and_unit_status(self, status: ops.StatusBase) -> None:
"""Update the application and unit status.
Args:
status: the desired application and unit status.
"""
self._charm.unit.status = status
if self._charm.unit.is_leader():
self._charm.app.status = status

def _restart_flask(self) -> None:
"""Restart or start the flask service if not started with the latest configuration."""
try:
restart_flask(
charm=self._charm, charm_state=self._charm_state, webserver=self._webserver
)
self._update_app_and_unit_status(ops.ActiveStatus())
except CharmConfigInvalidError as exc:
self._update_app_and_unit_status(ops.BlockedStatus(exc.msg))

def _on_database_requires_event(self, _event: DatabaseRequiresEvent) -> None:
"""Configure the flask pebble service layer in case of DatabaseRequiresEvent.
Args:
_event: the database-requires-changed event that trigger this callback function.
"""
self._flask_app.restart_flask()
self._restart_flask()

def _setup_database_requirer(self, relation_name: str, database_name: str) -> DatabaseRequires:
"""Set up a DatabaseRequires instance.
Expand Down
191 changes: 88 additions & 103 deletions src/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,111 +10,96 @@

from charm_state import KNOWN_CHARM_CONFIG, CharmState
from constants import FLASK_ENV_CONFIG_PREFIX, FLASK_SERVICE_NAME
from exceptions import CharmConfigInvalidError
from webserver import GunicornWebserver

logger = logging.getLogger(__name__)


class FlaskApp: # pylint: disable=too-few-public-methods
"""A class representing the Flask application."""

def __init__(
self, charm: ops.CharmBase, charm_state: CharmState, webserver: GunicornWebserver
):
"""Initialize a new instance of the FlaskApp class.
Args:
charm: The main charm object.
charm_state: The state of the charm that the FlaskApp instance belongs to.
webserver: The webserver manager object.
"""
self._charm = charm
self._charm_state = charm_state
self._webserver = webserver

def _flask_environment(self) -> dict[str, str]:
"""Generate a Flask environment dictionary from the charm Flask configurations.
The Flask environment generation follows these rules:
1. User-defined configuration cannot overwrite built-in Flask configurations, even if
the built-in Flask configuration value is None (undefined).
2. Boolean and integer-typed configuration values will be JSON encoded before
being passed to Flask.
3. String-typed configuration values will be passed to Flask as environment variables
directly.
Returns:
A dictionary representing the Flask environment variables.
"""
builtin_flask_config = [
c.removeprefix("flask_") for c in KNOWN_CHARM_CONFIG if c.startswith("flask_")
]
flask_env = {
k: v for k, v in self._charm_state.app_config.items() if k not in builtin_flask_config
}
flask_env.update(self._charm_state.flask_config)
env = {
f"{FLASK_ENV_CONFIG_PREFIX}{k.upper()}": v if isinstance(v, str) else json.dumps(v)
for k, v in flask_env.items()
}
secret_key_env = f"{FLASK_ENV_CONFIG_PREFIX}SECRET_KEY"
if secret_key_env not in env:
env[secret_key_env] = self._charm_state.flask_secret_key
for proxy_variable in ("http_proxy", "https_proxy", "no_proxy"):
proxy_value = getattr(self._charm_state.proxy, proxy_variable)
if proxy_value:
env[proxy_variable] = str(proxy_value)
env[proxy_variable.upper()] = str(proxy_value)
env.update(self._charm_state.database_uris)
return env

def _flask_layer(self) -> ops.pebble.LayerDict:
"""Generate the pebble layer definition for flask application.
Returns:
The pebble layer definition for flask application.
"""
environment = self._flask_environment()
return ops.pebble.LayerDict(
services={
FLASK_SERVICE_NAME: {
"override": "replace",
"summary": "Flask application service",
"command": shlex.join(self._webserver.command),
"startup": "enabled",
"environment": environment,
}
},
)

def _update_app_and_unit_status(self, status: ops.StatusBase) -> None:
"""Update the application and unit status.
Args:
status: the desired application and unit status.
"""
self._charm.unit.status = status
if self._charm.unit.is_leader():
self._charm.app.status = status

def restart_flask(self) -> None:
"""Restart or start the flask service if not started with the latest configuration."""
container = self._charm.unit.get_container("flask-app")
if not container.can_connect():
logger.info("pebble client in the Flask container is not ready")
return
if not self._charm_state.is_secret_storage_ready:
logger.info("secret storage is not initialized")
return
container.add_layer("flask-app", self._flask_layer(), combine=True)
is_webserver_running = container.get_service(FLASK_SERVICE_NAME).is_running()
try:
self._webserver.update_config(
flask_environment=self._flask_environment(),
is_webserver_running=is_webserver_running,
)
except CharmConfigInvalidError as exc:
self._update_app_and_unit_status(ops.BlockedStatus(exc.msg))
container.replan()
self._update_app_and_unit_status(ops.ActiveStatus())
def _flask_environment(charm_state: CharmState) -> dict[str, str]:
"""Generate a Flask environment dictionary from the charm Flask configurations.
Args:
charm_state: The state of the charm.
The Flask environment generation follows these rules:
1. User-defined configuration cannot overwrite built-in Flask configurations, even if
the built-in Flask configuration value is None (undefined).
2. Boolean and integer-typed configuration values will be JSON encoded before
being passed to Flask.
3. String-typed configuration values will be passed to Flask as environment variables
directly.
Returns:
A dictionary representing the Flask environment variables.
"""
builtin_flask_config = [
c.removeprefix("flask_") for c in KNOWN_CHARM_CONFIG if c.startswith("flask_")
]
flask_env = {k: v for k, v in charm_state.app_config.items() if k not in builtin_flask_config}
flask_env.update(charm_state.flask_config)
env = {
f"{FLASK_ENV_CONFIG_PREFIX}{k.upper()}": v if isinstance(v, str) else json.dumps(v)
for k, v in flask_env.items()
}
secret_key_env = f"{FLASK_ENV_CONFIG_PREFIX}SECRET_KEY"
if secret_key_env not in env:
env[secret_key_env] = charm_state.flask_secret_key
for proxy_variable in ("http_proxy", "https_proxy", "no_proxy"):
proxy_value = getattr(charm_state.proxy, proxy_variable)
if proxy_value:
env[proxy_variable] = str(proxy_value)
env[proxy_variable.upper()] = str(proxy_value)
env.update(charm_state.database_uris)
return env


def _flask_layer(charm_state: CharmState, webserver: GunicornWebserver) -> ops.pebble.LayerDict:
"""Generate the pebble layer definition for flask application.
Args:
charm_state: The state of the charm
webserver: The webserver manager object
Returns:
The pebble layer definition for flask application.
"""
environment = _flask_environment(charm_state=charm_state)
return ops.pebble.LayerDict(
services={
FLASK_SERVICE_NAME: {
"override": "replace",
"summary": "Flask application service",
"command": shlex.join(webserver.command),
"startup": "enabled",
"environment": environment,
}
},
)


def restart_flask(
charm: ops.CharmBase, charm_state: CharmState, webserver: GunicornWebserver
) -> None:
"""Restart or start the flask service if not started with the latest configuration.
Raise CharmConfigInvalidError if the configuration is not valid.
Args:
charm: The main charm object.
charm_state: The state of the charm.
webserver: The webserver manager object.
"""
container = charm.unit.get_container("flask-app")
if not container.can_connect():
logger.info("pebble client in the Flask container is not ready")
return
if not charm_state.is_secret_storage_ready:
logger.info("secret storage is not initialized")
return
container.add_layer("flask-app", _flask_layer(charm_state, webserver), combine=True)
is_webserver_running = container.get_service(FLASK_SERVICE_NAME).is_running()
webserver.update_config(
flask_environment=_flask_environment(charm_state),
is_webserver_running=is_webserver_running,
)
container.replan()
5 changes: 2 additions & 3 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from charm_state import KNOWN_CHARM_CONFIG, CharmState
from constants import FLASK_CONTAINER_NAME
from flask_app import FlaskApp
from flask_app import restart_flask
from webserver import GunicornWebserver

FLASK_BASE_DIR = "/srv/flask"
Expand All @@ -34,8 +34,7 @@ def test_flask_pebble_layer(harness: Harness) -> None:
charm_state=charm_state,
flask_container=harness.charm.unit.get_container(FLASK_CONTAINER_NAME),
)
flask_app = FlaskApp(charm=harness.charm, charm_state=charm_state, webserver=webserver)
flask_app.restart_flask()
restart_flask(charm=harness.charm, charm_state=charm_state, webserver=webserver)
flask_layer = harness.get_container_pebble_plan("flask-app").to_dict()["services"]["flask-app"]
assert flask_layer == {
"override": "replace",
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/test_databases.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ def test_database_uri_mocked(
send_signal_mock = unittest.mock.MagicMock()
monkeypatch.setattr(container, "send_signal", send_signal_mock)

databases = Databases(unittest.mock.MagicMock(), unittest.mock.MagicMock())
databases = Databases(
unittest.mock.MagicMock(), unittest.mock.MagicMock(), unittest.mock.MagicMock()
)
assert not databases.get_uris()

# Create the databases mock with the relation data
Expand Down
Loading

0 comments on commit 722c7b7

Please sign in to comment.