Skip to content
Draft
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
57 changes: 55 additions & 2 deletions linodecli/overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ def handle_databases_postgres_config_view(
return databases_postgres_config_view_output(json_data)


@output_override("databases", "valkey-config-view", OutputMode.table)
def handle_databases_valkey_config_view(
operation, output_handler, json_data
) -> bool:
# pylint: disable=unused-argument
"""
Override the output of 'linode-cli databases valkey-config-view'
to properly display the valkey engine config.
"""
return databases_valkey_config_view_output(json_data)


@output_override("domains", "zone-file", OutputMode.delimited)
def handle_domains_zone_file(operation, output_handler, json_data) -> bool:
# pylint: disable=unused-argument
Expand Down Expand Up @@ -317,11 +329,26 @@ def pg_view_output(json_data) -> bool:
return False


def add_param_row(output, param_name, param_data):
def _format_param_type(param_type) -> str:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: not sure how consistently the convention is followed in this repo, but in this module at least it looks like the step-down rule more-or-less applies, which means this should follow add_param_row().

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It looks like this is intentional, but it's worth noting in this context that the logic here makes the behavior for Valkey parameters with multiple types different from the other engines. I can't immediately produce an example for PostgreSQL, which is currently broken in the CLI (!), but I can for MySQL:

├────────────────────────────────┼────────────────────┼────────────────────┼─────────┼──────────────────────┼────────────┼────────────┼───────────────────────┼──────────────────┼────────────────────────────────┤
│ mysql.innodb_ft_server_stopwo… │ ['null', 'string'] │ db_name/table_name │         │                      │            │    1024    │        ^.+/.+$        │        NO        │ This option is used to specify │
│                                │                    │                    │         │                      │            │            │                       │                  │ your own InnoDB FULLTEXT index │
│                                │                    │                    │         │                      │            │            │                       │                  │ stopword list for all InnoDB   │
│                                │                    │                    │         │                      │            │            │                       │                  │ tables.                        │
├────────────────────────────────┼────────────────────┼────────────────────┼─────────┼──────────────────────┼────────────┼────────────┼───────────────────────┼──────────────────┼────────────────────────────────┤

... where the type of the parameter would be given as null | string for Valkey.

Maybe this is a question best addressed to the maintainers, but: what's the right thing to do here? Aim for consistency across the engine types? If so, should MySQL + (presumably) PG switch to the formatting applied here, or should Valkey use the existing formatting?

"""
Format a DB Config parameter type, which may be a list for nullable
parameters (e.g. ["integer", "null"] -> "integer | null").
"""
if isinstance(param_type, list):
return " | ".join(str(t) for t in param_type)

return str(param_type)


def add_param_row(output, param_name, param_data, format_nullable_type=False):
"""
Construct and add a row to the output table for DB Config view overrides.
"""
param_type = str(param_data.get("type", ""))
param_type = param_data.get("type", "")
if format_nullable_type:
param_type = _format_param_type(param_type)
else:
param_type = str(param_type)
example = str(param_data.get("example", ""))
minimum = str(param_data.get("minimum", ""))
maximum = str(param_data.get("maximum", ""))
Expand Down Expand Up @@ -409,3 +436,29 @@ def databases_postgres_config_view_output(json_data) -> bool:
console.print(output)

return False


def databases_valkey_config_view_output(json_data) -> bool:
"""
Parse and format the Valkey configuration output table.
"""
output = Table(header_style="bold", show_lines=True)

output.add_column("Parameter", style="bold")
output.add_column("Type", justify="center")
output.add_column("Example", justify="center")
output.add_column("Min", justify="center")
output.add_column("Max", justify="center")
output.add_column("Min Length", justify="center")
output.add_column("Max Length", justify="center")
output.add_column("Pattern", justify="center")
output.add_column("Requires Restart", justify="center")
output.add_column("Description", style="dim")

for field, params in json_data.items():
add_param_row(output, field, params, format_nullable_type=True)

console = Console()
console.print(output)

return False
97 changes: 97 additions & 0 deletions tests/integration/database/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import json

import pytest

from tests.integration.database.helpers import wait_for_database_status
from tests.integration.helpers import (
BASE_CMDS,
DEFAULT_REGION,
delete_target_id,
exec_test_command,
get_random_text,
Expand Down Expand Up @@ -76,3 +80,96 @@ def mysql_cluster():
delete_target_id(
target="databases", delete_command="mysql-delete", id=database_id
)


@pytest.fixture(scope="session")
def valkey_cluster():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The approach in valkey_cluster() feels like an improvement to me relative to the other database engines -- being flexible about the type, region, and engine; making sure the instance is ready before use; and sharing more details with the tests that consume the instance all seem like facially good, reasonable choices -- but there are tradeoffs that are worth explicitly considering, I think.

  • One is whether we want to leave the CLI in a state where Valkey is treated very differently from PostgreSQL and MySQL. That's potentially both a maintainability issue and a source of inconsistency in the quality of the products. A plausible answer to that is to line up PG and MySQL with Valkey, though -- either in this PR or in a followup -- rather than to move in the other direction.
  • Another is how comprehensive these tests are intended to be/should be. Some of the testing here arguably veers into testing the Managed Database functionality itself, which might be more than we want from these tests. The additional complexity and coverage here carries some maintenance burden that's worth taking into account. Probably this question is mostly for the maintainers.
  • Somewhat relatedly: some of these steps will nontrivially increase the running time of the tests. Probably not a huge deal, but FWIW it may mean we should be more aggressive about treating these tests as long-running.

commands = BASE_CMDS["databases"]
engines = json.loads(exec_test_command(commands + ["engines", "--json"]))
engine = next(
(item["id"] for item in engines if item["id"].startswith("valkey/")),
None,
)
assert engine is not None, "No Valkey engine is available"

types = json.loads(exec_test_command(commands + ["types", "--json"]))
plans = [
(item["id"], item["engines"]["price"]["hourly"])
for item in types
if item.get("_split") == "valkey" and item["engines"]["quantity"] == 1
]
assert plans, "No single-node Valkey database type is available"
plan = min(plans, key=lambda entry: entry[1])[0]

regions = json.loads(
exec_test_command(["linode-cli", "regions", "list", "--json"])
)
supported_regions = [
item["id"]
for item in regions
if "Managed Databases" in item["capabilities"]
and item.get("status") == "ok"
]
region = (
DEFAULT_REGION
if DEFAULT_REGION in supported_regions
else next(iter(supported_regions), None)
)
assert region is not None, "No available Managed Databases region found"

label = get_random_text(8) + "-valkey"
created = json.loads(
exec_test_command(
commands
+ [
"valkey-create",
"--engine",
engine,
"--label",
label,
"--region",
region,
"--type",
plan,
"--engine_config.valkey_timeout",
"301",
"--engine_config.valkey_active_expire_effort",
"2",
"--engine_config.valkey_lfu_decay_time",
"2",
"--engine_config.valkey_lfu_log_factor",
"11",
"--engine_config.valkey_maxmemory_policy",
"allkeys-lru",
"--no-defaults",
"--json",
]
)
)[0]
database_id = created["id"]

try:
wait_for_database_status("valkey", database_id, "active")
yield {
"id": database_id,
"engine": engine,
"label": label,
"region": region,
"type": plan,
"engine_config": {
"valkey_timeout": 301,
"valkey_active_expire_effort": 2,
"valkey_lfu_decay_time": 2,
"valkey_lfu_log_factor": 11,
"valkey_maxmemory_policy": "allkeys-lru",
},
}
finally:
delete_target_id(
target="databases",
delete_command="valkey-delete",
id=str(database_id),
use_retry=True,
retries=5,
delay=15,
)
37 changes: 37 additions & 0 deletions tests/integration/database/helpers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
import json
import subprocess

from tests.integration.helpers import (
BASE_CMDS,
exec_test_command,
wait_for_condition,
)


def database_command(action, *args):
return BASE_CMDS["databases"] + [action, *map(str, args)]


def database_json(action, *args):
return json.loads(
exec_test_command(database_command(action, *args, "--json"))
)


def private_database_json(action, database_id):
process = subprocess.run(
database_command(action, database_id, "--json"),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
assert (
process.returncode == 0
), f"{action} failed (exit {process.returncode})"
return json.loads(process.stdout)[0]


def wait_for_database_status(engine, database_id, status):
wait_for_condition(
15,
900,
lambda: database_json(f"{engine}-view", database_id)[0]["status"]
== status,
)


def get_db_type_id():
db_type_ids = exec_test_command(
BASE_CMDS["databases"]
Expand Down
Loading
Loading