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
9 changes: 8 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ run-all COVERAGE="false":
echo "=== Running Parameter Tests ==="
{{PYTEST}} {{DIR}}/main/test_parameter.py -v $COVERAGE_FLAGS

echo "=== Running Database Tests ==="
{{PYTEST}} {{DIR}}/main/test_database.py -v $COVERAGE_FLAGS

echo "All tests completed successfully!"
if [ "{{COVERAGE}}" = "true" ]; then
echo "Coverage report generated in htmlcov/ directory"
Expand Down Expand Up @@ -107,12 +110,16 @@ run RESOURCE COVERAGE="false":
echo "Running Parameter tests..."
{{PYTEST}} {{DIR}}/main/test_parameter.py -v $COVERAGE_FLAGS
;;
"database"|"databases"|"db")
echo "Running Database tests..."
{{PYTEST}} {{DIR}}/main/test_database.py -v $COVERAGE_FLAGS
;;
"all")
just run-all {{COVERAGE}}
;;
*)
echo "Unknown resource type: {{RESOURCE}}"
echo "Available resources: auth, project, package, secret, staticroute, service-account, deployment, usergroup, parameter"
echo "Available resources: auth, project, package, secret, staticroute, service-account, deployment, usergroup, parameter, database"
echo "Use 'just run all' to run all tests"
echo "Add 'true' as second argument to enable coverage: just run <resource> true"
exit 1
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ dependencies = [
"waiting>=1.4.1",
"yaspin==2.5.0",
"ansible-core>=2.13.13",
"rapyuta-io-sdk-v2>=0.7.0",
"rapyuta-io-sdk-v2 @ git+https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2.git@feat/native-db-sdk-clients",
Comment thread
rAJ-2301 marked this conversation as resolved.
"typing-extensions>=4.15.0",
"python-benedict==0.30",
]
Expand Down
38 changes: 38 additions & 0 deletions riocli/apply/manifests/database.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
apiVersion: "api.rapyuta.io/v2"
kind: "Database"
metadata:
name: "my-postgres-db"
labels:
app: my-app
spec:
type: "postgres" # Required. Only supported engine: postgres
postgres:
version: "" # Required. Use `rio database versions` to list supported versions.
primary:
deviceName: "my-device" # Required. Name of the primary device.
dataDirectory: "" # Optional. Defaults to /opt/rapyuta/volumes/postgres_<name>/<version>
credentials:
username: "admin" # Required.
password: "changeme" # Required.
multipleDatabase: # Optional. Additional databases to create inside the instance.
- "app_db"
parameters: # Optional. PostgreSQL server configuration parameters.
max_connections: "200"
shared_buffers: "256MB"
migration: # Optional. In-place data migration from an older data directory.
enabled: false
sourceDataDirectory: ""
standby: # Optional. Standby (replication) configuration.
primaryInterface: "eth0"
devices:
- deviceName: "my-standby-device"
dataDirectory: "" # Optional. Defaults to /opt/rapyuta/volumes/postgres_<name>/standby/<version>
backup: # Optional. Backup configuration.
enabled: true
schedule: "0 2 * * *" # Required when enabled. Standard cron format.
directory: "/backups/my-postgres-db" # Optional. Defaults to /backups/<name>
credentials: # Required when enabled.
username: "replicator"
password: "changeme"
recovery: # Optional. Point-in-time recovery from an existing backup.
sourceBackupID: "" # Required when recovery block is present.
2 changes: 2 additions & 0 deletions riocli/apply/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from riocli.config import get_config_from_context
from riocli.constants import Colors
from riocli.constants.symbols import Symbols
from riocli.database.model import Database
from riocli.deployment.model import Deployment
from riocli.device.model import Device
from riocli.disk.model import Disk
Expand All @@ -42,6 +43,7 @@
from riocli.utils import tabulate_data

KIND_TO_CLASS = {
"database": Database,
"deployment": Deployment,
"device": Device,
"disk": Disk,
Expand Down
3 changes: 3 additions & 0 deletions riocli/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from riocli.config.context import cli_context
from riocli.configtree import config_trees
from riocli.constants import Colors, Symbols
from riocli.database import database
from riocli.deployment import deployment
from riocli.device import device
from riocli.disk import disk
Expand Down Expand Up @@ -66,6 +67,7 @@
"sr": "static-route",
"ug": "usergroup",
"sa": "service-account",
"db": "database",
},
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
Expand Down Expand Up @@ -175,3 +177,4 @@ def update(silent: bool) -> None:
cli.add_command(role)
cli.add_command(service_account)
cli.add_command(permission)
cli.add_command(database)
Comment on lines 177 to +180

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

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

New rio database command group is added to the root CLI, but there are no corresponding tests under tests/ (no database mentions in tests/main), while other command groups have integration/RBAC coverage. Add at least basic CLI tests (list/inspect/delete/backup/versions) and apply-kind coverage for Database to prevent regressions.

Copilot uses AI. Check for mistakes.
27 changes: 27 additions & 0 deletions riocli/database/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import click

from riocli.constants import Colors
from riocli.database.backup import backup
from riocli.database.delete import delete_database
from riocli.database.inspect import inspect_database
from riocli.database.list import list_databases
from riocli.database.versions import list_database_versions
from riocli.utils import AliasedGroup


@click.group(
invoke_without_command=False,
cls=AliasedGroup,
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
def database() -> None:
"""Manage database resources."""
pass


database.add_command(list_databases)
database.add_command(inspect_database)
database.add_command(delete_database)
database.add_command(list_database_versions)
database.add_command(backup)
190 changes: 190 additions & 0 deletions riocli/database/backup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import click
from click_help_colors import HelpColorsCommand

from riocli.config import new_v2_client
from riocli.constants import Colors, Symbols
from riocli.utils import AliasedGroup, inspect_with_format, tabulate_data
from riocli.utils.spinner import with_spinner


@click.group(
"backup",
invoke_without_command=False,
cls=AliasedGroup,
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
def backup() -> None:
"""Manage backups for a database."""
pass


@backup.command(
"list",
cls=HelpColorsCommand,
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
@click.argument("database-name")
def list_backups(database_name: str) -> None:
"""List backups for a database.

Usage Examples:

$ rio database backup list DATABASE_NAME
"""
try:
client = new_v2_client(with_project=True)
result = client.list_backups(database_name=database_name)
backups = result.items or []
_display_backup_list(backups, show_header=True)
except Exception as e:
click.secho(str(e), fg=Colors.RED)
raise SystemExit(1)


def _display_backup_list(backups: list, show_header: bool = True) -> None:
headers = []
if show_header:
headers = ["Backup ID", "Database", "Type", "Version", "Upload Status"]

data = []
for b in backups:
data.append(
[
b.spec.id if b.spec else "",
b.spec.databaseName if b.spec else "",
b.spec.type if b.spec else "",
b.spec.version if b.spec else "",
b.spec.status if b.spec else "",
]
)

tabulate_data(data, headers)


@backup.command(
"inspect",
cls=HelpColorsCommand,
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
@click.option(
"--format",
"-f",
"format_type",
default="yaml",
type=click.Choice(["json", "yaml"], case_sensitive=False),
)
@click.argument("database-name")
@click.argument("backup-id")
def inspect_backup(format_type: str, database_name: str, backup_id: str) -> None:
"""Inspect a database backup.

Prints the backup resource in the specified format.
The supported formats are ``json`` and ``yaml``. Default is ``yaml``.

Usage Examples:

$ rio database backup inspect DATABASE_NAME BACKUP_ID
"""
try:
client = new_v2_client()
b = client.get_backup(database_name=database_name, backup_id=backup_id)

if not b:
click.secho("backup not found", fg=Colors.RED)
raise SystemExit(1)

inspect_with_format(b.model_dump(exclude_none=True, by_alias=True), format_type)
except Exception as e:
click.secho(str(e), fg=Colors.RED)
raise SystemExit(1)


@backup.command(
"delete",
cls=HelpColorsCommand,
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
@click.option(
"--force", "-f", "--silent", is_flag=True, default=False, help="Skip confirmation"
)
@click.argument("database-name")
@click.argument("backup-id")
@with_spinner(text="Deleting backup...")
def delete_backup(
force: bool,
database_name: str,
backup_id: str,
spinner=None,
) -> None:
"""Delete a backup for a database.

Usage Examples:

$ rio database backup delete DATABASE_NAME BACKUP_ID
"""
client = new_v2_client()

if not force:
with spinner.hidden():
click.confirm(
f"Do you want to delete backup {backup_id!r} for database {database_name!r}?",
default=True,
abort=True,
)

try:
client.delete_backup(database_name=database_name, backup_id=backup_id)
spinner.text = click.style("Backup deleted successfully.", Colors.GREEN)
spinner.ok(click.style(Symbols.SUCCESS, Colors.GREEN))
except Exception as e:
spinner.text = click.style(f"Failed to delete backup: {e}", Colors.RED)
spinner.red.fail(Symbols.ERROR)
raise SystemExit(1) from e


@backup.command(
"restore",
cls=HelpColorsCommand,
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
@click.option(
"--force", "-f", "--silent", is_flag=True, default=False, help="Skip confirmation"
)
@click.argument("database-name")
@click.argument("backup-id")
@with_spinner(text="Restoring backup...")
def restore_backup(
force: bool,
database_name: str,
backup_id: str,
spinner=None,
) -> None:
"""Restore a database from a backup.

Usage Examples:

$ rio database backup restore DATABASE_NAME BACKUP_ID
"""
client = new_v2_client()

if not force:
with spinner.hidden():
click.confirm(
f"Do you want to restore database {database_name!r} from backup {backup_id!r}?",
default=True,
abort=True,
)

try:
client.restore_backup(database_name=database_name, backup_id=backup_id)
spinner.text = click.style("Restore initiated successfully.", Colors.GREEN)
spinner.ok(click.style(Symbols.SUCCESS, Colors.GREEN))
except Exception as e:
spinner.text = click.style(f"Failed to restore backup: {e}", Colors.RED)
spinner.red.fail(Symbols.ERROR)
raise SystemExit(1) from e
Loading
Loading