Skip to content
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

[DPE-6062] Allow --restore-to-time=latest without a backup-id #683

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 15 additions & 5 deletions src/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,8 @@ def _get_nearest_timeline(self, timestamp: str) -> tuple[str, str] | None:
(stanza, timeline) of the nearest timeline or backup. None, if there are no matches.
"""
timelines = self._list_backups(show_failed=False) | self._list_timelines()
if timestamp == "latest":
return max(timelines.items())[1] if len(timelines) > 0 else None
filtered_timelines = [
(timeline_key, timeline_object)
for timeline_key, timeline_object in timelines.items()
Expand Down Expand Up @@ -1026,6 +1028,17 @@ def _on_restore_action(self, event): # noqa: C901
elif is_backup_id_timeline:
restore_stanza_timeline = timelines[backup_id]
else:
backups_list = list(self._list_backups(show_failed=False).values())
timelines_list = self._list_timelines()
if (
restore_to_time == "latest"
and timelines_list is not None
and max(timelines_list.values() or [backups_list[0]]) not in backups_list
):
error_message = "There is no base backup created from the latest timeline"
logger.error(f"Restore failed: {error_message}")
event.fail(error_message)
return
restore_stanza_timeline = self._get_nearest_timeline(restore_to_time)
if not restore_stanza_timeline:
error_message = f"Can't find the nearest timeline before timestamp {restore_to_time} to restore"
Expand Down Expand Up @@ -1150,11 +1163,8 @@ def _pre_restore_checks(self, event: ActionEvent) -> bool:
event.fail(validation_message)
return False

if not event.params.get("backup-id") and event.params.get("restore-to-time") in (
None,
"latest",
):
error_message = "Missing backup-id or non-latest restore-to-time parameter to be able to do restore"
if not event.params.get("backup-id") and event.params.get("restore-to-time") is None:
error_message = "Either backup-id or restore-to-time parameters need to be provided to be able to do restore"
logger.error(f"Restore failed: {error_message}")
event.fail(error_message)
return False
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/test_backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,10 @@ def test_get_nearest_timeline(harness):
_list_timelines.return_value = dict[str, tuple[str, str]]({
"2023-02-24T05:00:00Z": ("test-stanza", "2")
})
assert harness.charm.backup._get_nearest_timeline("latest") == tuple[str, str]((
"test-stanza",
"2",
))
assert harness.charm.backup._get_nearest_timeline("2025-01-01 00:00:00") == tuple[
str, str
](("test-stanza", "2"))
Expand Down Expand Up @@ -1567,6 +1571,49 @@ def test_on_restore_action(harness):
mock_event.fail.assert_not_called()
mock_event.set_results.assert_called_once_with({"restore-status": "restore started"})

# Test a failed PITR with only the restore-to-time parameter equal to latest
# (it should fail when there is no base backup created from the latest timeline).
mock_event.reset_mock()
_empty_data_files.reset_mock()
with harness.hooks_disabled():
harness.update_relation_data(
peer_rel_id,
harness.charm.app.name,
{
"restore-timeline": "",
"restore-to-time": "",
"restore-stanza": "",
},
)
_update_config.reset_mock()
mock_event.params = {"restore-to-time": "latest"}
harness.charm.backup._on_restore_action(mock_event)
_empty_data_files.assert_not_called()
_restart_database.assert_not_called()
assert harness.get_relation_data(peer_rel_id, harness.charm.app) == {}
_update_config.assert_not_called()
mock_event.set_results.assert_not_called()
mock_event.fail.assert_called_once()

# Test a successful PITR with only the restore-to-time parameter equal to latest.
mock_event.reset_mock()
mock_event.params = {"restore-to-time": "latest"}
_list_backups.return_value = {
"2023-01-01T09:00:00Z": (harness.charm.backup.stanza_name, "1"),
"2024-02-24T05:00:00Z": (harness.charm.backup.stanza_name, "2"),
}
harness.charm.backup._on_restore_action(mock_event)
_empty_data_files.assert_called_once()
_restart_database.assert_not_called()
assert harness.get_relation_data(peer_rel_id, harness.charm.app) == {
"restore-timeline": "2",
"restore-to-time": "latest",
"restore-stanza": f"{harness.charm.model.name}.{harness.charm.cluster_name}",
}
_update_config.assert_called_once()
mock_event.fail.assert_not_called()
mock_event.set_results.assert_called_once_with({"restore-status": "restore started"})


def test_pre_restore_checks(harness):
with (
Expand Down