Skip to content
Open
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
1 change: 1 addition & 0 deletions .rio
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"configtrees": {"project-da78q6sqp38c73f8ikhg": {"test1": {"rev_id": "rev-da78rjdugeis739lu3m0", "committed": true}}}}
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ dependencies = [
"waiting>=1.4.1",
"yaspin==2.5.0",
"ansible-core>=2.13.13",
"rapyuta-io-sdk-v2 @ git+https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2@feat/managed-database-phase-3",
"rapyuta-io-sdk-v2 @ git+https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2@feat/managed-database-phase-4",
"cryptography>=40.0.0",
"typing-extensions>=4.15.0",
"python-benedict==0.30",
Expand Down Expand Up @@ -204,3 +204,5 @@ filterwarnings = [
"ignore::PendingDeprecationWarning",
]

[tool.uv.sources]
rapyuta-io-sdk-v2 = { git = "https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2", branch = "feat/managed-database-phase-4" }
48 changes: 48 additions & 0 deletions riocli/apply/manifests/restore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
apiVersion: "api.rapyuta.io/v2"
kind: "Restore"
metadata:
name: "orders-db-restore"
labels:
app: orders
spec:
# Target database. It must already exist and be Running: a restore loads into
# the live instance, it never creates one. A second restore against the same
# database is refused while one is still in flight.
database: orders-db
source:
type: backup # Supported: backup, dataDirectory
# --- type: backup ---
# The archive to restore, by file-upload GUID or filename. A backup produces
# many archives over its life, so the one to load is named directly.
# `rio device uploads list <device>` shows both columns.
fileUpload: fileupload-d9upialugeis73e1to40
# Both are derived from the upload's own metadata and can be omitted. Set
# them only to override: backupName selects the restore image's major
# version, and the pair is what lets the device find the backup in its own
# barman store instead of downloading the archive back.
# backupName: orders-nightly
# backupRunID: "20260101T020000"
# Point in time to roll forward to. Honoured only when the device still
# holds this backup in its barman store, which keeps the WAL archive
# alongside it; an uploaded archive is a fixed point and the device refuses
# the request rather than restoring to the wrong one.
# targetTime: "2026-01-01T02:00:00Z"
#
# --- type: dataDirectory ---
# A major-version migration is expressed as a restore from the old cluster's
# data directory, which is the database's dataDirectory with the major
# version appended. Must be absolute: Docker reads a relative path as a
# named volume, so the restore would run against an empty directory. The old
# cluster is copied, never opened in place.
# oldDataDirectory: /opt/rapyuta/volumes/orders-db/17
# The old cluster's major version. It resolves the restore image, and the
# image cross-checks it against the directory's PG_VERSION.
# sourceVersion: "17"
# Logical databases to load. Omit to restore every one the source holds;
# anything outside this list is left untouched in the target.
databases:
- orders
options:
clean: true # Drop the objects being restored before recreating them
noOwner: true # Restore without the source's ownership
ifExists: true # Tolerate absent objects; only meaningful with clean
2 changes: 2 additions & 0 deletions riocli/apply/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from riocli.constants import Colors
from riocli.constants.symbols import Symbols
from riocli.database.model import Database
from riocli.database.restore.model import Restore
from riocli.deployment.model import Deployment
from riocli.device.model import Device
from riocli.disk.model import Disk
Expand All @@ -46,6 +47,7 @@
KIND_TO_CLASS = {
"database": Database,
"backup": Backup,
"restore": Restore,
"deployment": Deployment,
"device": Device,
"disk": Disk,
Expand Down
17 changes: 15 additions & 2 deletions riocli/backup/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,23 @@ def fetch_backups(
def display_backup_list(backups: typing.Any, show_header: bool = True):
headers = []
if show_header:
headers = ("GUID", "Name", "Type", "Database", "Schedule", "Phase")
headers = (
"GUID",
"Name",
"Type",
"Database",
"Schedule",
"Phase",
"Step",
)

data = []
for backup in backups:
phase = getattr(backup.status, "phase", None) if backup.status else None
status = backup.status
phase = getattr(status, "phase", None) if status else None
# The recover dominates a run, so the phase alone cannot tell a slow
# backup from a stuck one.
step = getattr(status, "step", None) if status else None
data.append(
[
backup.metadata.guid,
Expand All @@ -56,6 +68,7 @@ def display_backup_list(backups: typing.Any, show_header: bool = True):
backup.spec.database,
backup.spec.schedule or "-",
phase or "Unknown",
step or "-",
]
)

Expand Down
7 changes: 6 additions & 1 deletion riocli/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from riocli.database.delete import delete_database
from riocli.database.inspect import inspect_database
from riocli.database.list import list_databases
from riocli.database.restore import restore
from riocli.database.upload import upload
from riocli.utils import AliasedGroup


Expand All @@ -30,7 +32,8 @@
def database() -> None:
"""Manage PostgreSQL databases.

Create, list, inspect, and delete managed databases.
Create, list, inspect, and delete managed databases, and restore data
into a running one with ``rio database restore``.
Use ``rio apply`` to create or update databases from a manifest file.
"""
pass
Expand All @@ -39,3 +42,5 @@ def database() -> None:
database.add_command(list_databases)
database.add_command(inspect_database)
database.add_command(delete_database)
database.add_command(restore)
database.add_command(upload)
49 changes: 49 additions & 0 deletions riocli/database/restore/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright 2025 Rapyuta Robotics
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import click

from riocli.constants import Colors
from riocli.database.restore.create import create_restore
from riocli.database.restore.inspect import inspect_restore
from riocli.database.restore.list import list_restores
from riocli.utils import AliasedGroup


@click.group(
invoke_without_command=False,
cls=AliasedGroup,
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
def restore() -> None:
"""Restore data into a running database.

A restore loads logical databases into a live, running database, from either
a backup or an old on-device data directory. The latter is how a
major-version migration is done: create the new database, then restore into
it from the old data directory.

A restore never creates a database, and never touches logical databases
outside the ones it is given. Restores are sub-resources of a database, so
every command takes the target with --database.

Use ``rio apply`` to run a restore from a manifest file.
"""
pass


restore.add_command(list_restores)
restore.add_command(inspect_restore)
restore.add_command(create_restore)
Loading
Loading