From fc9c2b6eafd99e12eb547ac0795aa9d7e699acd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20U=C4=9EUR?= <39213991+alithethird@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:33:15 +0300 Subject: [PATCH] Create a charm action to run wp core update db after a wordpress upgrade (#235) * Added the `update-database` action. I need feedback and advice on what kind of tests I need to write. * Added `dry-run` parameter * Wrote unit tests for `update-database` action * Removed blank lines after docstring * Fixed error message if action fails. * Making sure stderr exists and a str in action fail * Fixed number of commands in mock * Updated uni tests * Removed unnecessary else * Better debug and auto update-database when charm upgrades * Fix forgotten docstring * Removed empty line after docstring. * Better handle parameters get * Fixed docstring * Make linter happy. * Wrote how-to document for charm update * Add final new line * Removed update function from on-upgrade event, private function rearrangement, function re-order. * Document updates * Initialized test fail parameter and updated docstring * Fixed private function returning wrong type. * Fixed mock return results for database update * Happy linter happy life * Better documentation --- actions.yaml | 8 +++++ docs/how-to/upgrade-wordpress-charm.md | 20 ++++++++++++ src/charm.py | 44 ++++++++++++++++++++++++++ tests/unit/test_charm.py | 41 ++++++++++++++++++++++++ tests/unit/wordpress_mock.py | 8 +++++ 5 files changed, 121 insertions(+) create mode 100644 docs/how-to/upgrade-wordpress-charm.md diff --git a/actions.yaml b/actions.yaml index 8a12b36e..acbb6d3e 100644 --- a/actions.yaml +++ b/actions.yaml @@ -11,3 +11,11 @@ rotate-wordpress-secrets: auth_key, auth_salt, logged_in_key, logged_in_salt, nonce_key, nonce_salt, secure_auth_key, secure_auth_salt. Users will be forced to log in again. This might be useful under security breach circumstances. +update-database: + description: > + After upgrading WordPress to a new version it is typically necessary to run 'wp core update-db' + to migrate the database schema. This action does exactly that. + params: + dry-run: + type: boolean + description: Runs the 'wp core update-db --dry-run' command. diff --git a/docs/how-to/upgrade-wordpress-charm.md b/docs/how-to/upgrade-wordpress-charm.md new file mode 100644 index 00000000..ca5e140e --- /dev/null +++ b/docs/how-to/upgrade-wordpress-charm.md @@ -0,0 +1,20 @@ +# How to upgrade WordPress Charm + +Before updating the Charm you need to backup the database using MySQL charm's `create-backup` action. + +``` +juju run mysql/leader create-backup +``` +Additional info can be found about backup in [the MySQL documentation](https://charmhub.io/mysql/docs/h-create-and-list-backups) + +Then you can upgrade the WordPress Charm. + +``` +juju refresh wordpress-k8s +``` + +After upgrading the WordPress Charm you need to update the database schema. + +``` +juju run wordpress-k8s/0 update-database +``` diff --git a/src/charm.py b/src/charm.py index ec63b608..b6b6c3d2 100755 --- a/src/charm.py +++ b/src/charm.py @@ -182,6 +182,7 @@ def __init__(self, *args, **kwargs): self.framework.observe( self.on.rotate_wordpress_secrets_action, self._on_rotate_wordpress_secrets_action ) + self.framework.observe(self.on.update_database_action, self._on_update_database_action) self.framework.observe(self.on.leader_elected, self._setup_replica_data) self.framework.observe(self.on.uploads_storage_attached, self._reconciliation) @@ -267,6 +268,49 @@ def _on_rotate_wordpress_secrets_action(self, event: ActionEvent): self._reconciliation(event) event.set_results({"result": "ok"}) + def _on_update_database_action(self, event: ActionEvent): + """Handle the update-database action. + + This action is to upgrade the database schema after the WordPress version is upgraded. + + Args: + event: Used for returning result or failure of action. + """ + logger.info("Starting Database update process.") + result = self._update_database(bool(event.params.get("dry-run"))) + if result.success: + logger.info("Finished Database update process.") + event.set_results({"result": result.message}) + return + logger.error("Failed to update database schema: %s", result.message) + event.fail(result.message) + + def _update_database(self, dry_run: bool = False) -> types_.ExecResult: + """Update database. + + Args: + dry_run (bool, optional): Runs update as a dry-run, useful to check + if update is necessary without doing the update. Defaults to False. + + Returns: + Execution result. + """ + cmd = ["wp", "core", "update-db"] + if dry_run: + cmd.append("--dry-run") + + result = self._run_wp_cli(cmd, timeout=600) + if result.return_code != 0: + return types_.ExecResult( + success=False, + result=None, + message=str(result.stderr) if result.stderr else "Database update failed", + ) + logger.info("Finished Database update process.") + return types_.ExecResult( + success=True, result=None, message=str(result.stdout) if result.stdout else "ok" + ) + @staticmethod def _wordpress_secret_key_fields(): """Field names of secrets required for instantiation of WordPress. diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 99e059d9..7413bd45 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -476,6 +476,47 @@ def test_rotate_wordpress_secrets( action_event_mock.fail.assert_not_called() +def test_update_database( + patch, + harness: ops.testing.Harness, + action_event_mock: unittest.mock.MagicMock, +): + """ + arrange: after charm is initialized and database ready. + act: run update-database action. + assert: update-database action should success and return "ok". + """ + harness.set_can_connect(harness.model.unit.containers["wordpress"], True) + harness.begin_with_initial_hooks() + patch.container._fail_wp_update_database = False + charm: WordpressCharm = typing.cast(WordpressCharm, harness.charm) + charm._on_update_database_action(action_event_mock) + + action_event_mock.set_results.assert_called_once_with({"result": "ok"}) + action_event_mock.fail.assert_not_called() + + +def test_update_database_fail( + patch, + harness: ops.testing.Harness, + action_event_mock: unittest.mock.MagicMock, +): + """ + arrange: after charm is initialized and database is mocked to fail. + act: run update-database action. + assert: update-database action should fail. + """ + harness.set_can_connect(harness.model.unit.containers["wordpress"], True) + harness.begin_with_initial_hooks() + patch.container._fail_wp_update_database = True + charm: WordpressCharm = typing.cast(WordpressCharm, harness.charm) + action_event_mock.configure_mock() + charm._on_update_database_action(action_event_mock) + + action_event_mock.set_results.assert_not_called() + action_event_mock.fail.assert_called_once_with("Database update failed") + + @pytest.mark.usefixtures("attach_storage") def test_theme_reconciliation( patch: WordpressPatch, diff --git a/tests/unit/wordpress_mock.py b/tests/unit/wordpress_mock.py index 1e25e697..965d9363 100644 --- a/tests/unit/wordpress_mock.py +++ b/tests/unit/wordpress_mock.py @@ -359,6 +359,7 @@ def __init__( self._wordpress_database_mock = wordpress_database_mock self.installed_plugins = set(WordpressCharm._WORDPRESS_DEFAULT_PLUGINS) self.installed_themes = set(WordpressCharm._WORDPRESS_DEFAULT_THEMES) + self._fail_wp_update_database = False def exec( self, cmd, user=None, group=None, working_dir=None, combine_stderr=None, timeout=None @@ -645,6 +646,13 @@ def _mock_chown_uploads(self, _cmd): """Simulate ``chown`` command execution in the container.""" return ExecProcessMock(return_code=0, stdout="", stderr="") + @_exec_handler.register(lambda cmd: cmd[:3] == ["wp", "core", "update-db"]) + def _mock_wp_update_database(self, _cmd): + """Simulate ``wp core update-db`` command execution in the container.""" + if self._fail_wp_update_database: + return ExecProcessMock(return_code=1, stdout="", stderr="Database update failed") + return ExecProcessMock(return_code=0, stdout="ok", stderr="") + def __getattr__(self, item): """Passthrough anything else to :class:`ops.charm.model.Container`.