Skip to content

Commit 8bd17f7

Browse files
authored
feat(metactl): add lua subcommand with file and stdin support (#18437)
Add new `lua` subcommand to databend-metactl that executes Lua scripts from either files or stdin input. This enables scripting capabilities for meta service operations and automation. - Add mlua dependency with Lua 5.4 and vendored features - Create comprehensive lua test suite with file and stdin tests - Enhance test runner with selective test execution support Usage examples: ``` databend-metactl lua --file script.lua echo 'print("hello")' | databend-metactl lua ``` `databend-metactl lua` will be used for complicated benchmark scripting.
1 parent 91e76f1 commit 8bd17f7

File tree

7 files changed

+200
-9
lines changed

7 files changed

+200
-9
lines changed

Cargo.lock

Lines changed: 74 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ match-template = "0.0.1"
387387
md-5 = "0.10.5"
388388
memchr = { version = "2", default-features = false }
389389
micromarshal = "0.7.0"
390+
mlua = { version = "0.11", features = ["lua54", "vendored"] }
390391
mockall = "0.11.2"
391392
mysql_async = { version = "0.34", default-features = false, features = ["native-tls-tls"] }
392393
naive-cityhash = "0.2.0"

src/meta/binaries/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ display-more = { workspace = true }
4747
fastrace = { workspace = true }
4848
futures = { workspace = true }
4949
log = { workspace = true }
50+
mlua = { workspace = true }
5051
rand = { workspace = true }
5152
serde = { workspace = true }
5253
serde_json = { workspace = true }

src/meta/binaries/metactl/main.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#![allow(clippy::uninlined_format_args)]
1616

1717
use std::collections::BTreeMap;
18+
use std::io::Read;
19+
use std::io::{self};
1820
use std::sync::Arc;
1921
use std::time::Duration;
2022

@@ -32,6 +34,7 @@ use databend_common_meta_control::args::GetArgs;
3234
use databend_common_meta_control::args::GlobalArgs;
3335
use databend_common_meta_control::args::ImportArgs;
3436
use databend_common_meta_control::args::ListFeatures;
37+
use databend_common_meta_control::args::LuaArgs;
3538
use databend_common_meta_control::args::SetFeature;
3639
use databend_common_meta_control::args::StatusArgs;
3740
use databend_common_meta_control::args::TransferLeaderArgs;
@@ -50,6 +53,7 @@ use databend_common_tracing::FileConfig;
5053
use databend_meta::version::METASRV_COMMIT_VERSION;
5154
use display_more::DisplayOptionExt;
5255
use futures::stream::TryStreamExt;
56+
use mlua::Lua;
5357
use serde::Deserialize;
5458

5559
#[derive(Debug, Deserialize, Parser)]
@@ -226,6 +230,24 @@ impl App {
226230
Ok(())
227231
}
228232

233+
async fn run_lua(&self, args: &LuaArgs) -> anyhow::Result<()> {
234+
let lua = Lua::new();
235+
236+
let script = match &args.file {
237+
Some(path) => std::fs::read_to_string(path)?,
238+
None => {
239+
let mut buffer = String::new();
240+
io::stdin().read_to_string(&mut buffer)?;
241+
buffer
242+
}
243+
};
244+
245+
if let Err(e) = lua.load(&script).exec() {
246+
return Err(anyhow::anyhow!("Lua execution error: {}", e));
247+
}
248+
Ok(())
249+
}
250+
229251
fn new_grpc_client(&self, addresses: Vec<String>) -> Result<Arc<ClientHandle>, CreationError> {
230252
eprintln!(
231253
"Using gRPC API address: {}",
@@ -255,6 +277,7 @@ enum CtlCommand {
255277
Watch(WatchArgs),
256278
Upsert(UpsertArgs),
257279
Get(GetArgs),
280+
Lua(LuaArgs),
258281
}
259282

260283
/// Usage:
@@ -326,6 +349,9 @@ async fn main() -> anyhow::Result<()> {
326349
CtlCommand::Get(args) => {
327350
app.get(args).await?;
328351
}
352+
CtlCommand::Lua(args) => {
353+
app.run_lua(args).await?;
354+
}
329355
},
330356
// for backward compatibility
331357
None => {

src/meta/control/src/args.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,10 @@ pub struct TriggerSnapshotArgs {
263263
#[clap(long, default_value = "127.0.0.1:28101")]
264264
pub admin_api_address: String,
265265
}
266+
267+
#[derive(Debug, Clone, Deserialize, Args)]
268+
pub struct LuaArgs {
269+
/// Path to the Lua script file. If not provided, script is read from stdin
270+
#[clap(long)]
271+
pub file: Option<String>,
272+
}

tests/metactl/subcommands/cmd_lua.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python3
2+
3+
import subprocess
4+
import tempfile
5+
import os
6+
from metactl_utils import metactl_bin
7+
from utils import print_title
8+
9+
10+
def test_lua_file():
11+
"""Test lua subcommand with file input."""
12+
print_title("Test lua subcommand with file")
13+
14+
# Create a temporary Lua script
15+
with tempfile.NamedTemporaryFile(mode='w', suffix='.lua', delete=False) as f:
16+
f.write('print(2 + 3)\n')
17+
lua_file = f.name
18+
19+
print("file name:", lua_file)
20+
with open(lua_file, 'r') as ff:
21+
print("file content:", ff.read())
22+
23+
# Run metactl lua with file
24+
result = subprocess.run([
25+
metactl_bin, "lua",
26+
"--file", lua_file
27+
], capture_output=True, text=True, check=True)
28+
29+
output = result.stdout.strip()
30+
assert "5" == output
31+
print("✓ Lua file execution test passed")
32+
33+
34+
35+
def test_lua_stdin():
36+
"""Test lua subcommand with stdin input."""
37+
print_title("Test lua subcommand with stdin")
38+
39+
lua_script = 'print("Hello from stdin!")\nprint(5 * 6)\n'
40+
41+
# Run metactl lua with stdin
42+
result = subprocess.run([
43+
metactl_bin, "lua"
44+
], input=lua_script, capture_output=True, text=True, check=True)
45+
46+
output = result.stdout.strip()
47+
assert "Hello from stdin!" in output
48+
assert "30" in output
49+
print("✓ Lua stdin execution test passed")
50+
51+
52+
def main():
53+
"""Main function to run all lua tests."""
54+
test_lua_file()
55+
test_lua_stdin()
56+
57+
58+
if __name__ == "__main__":
59+
main()

tests/metactl/test_all_subcommands.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
88
Usage:
99
python tests/metactl/test_all_subcommands.py # Run all tests
10+
python tests/metactl/test_all_subcommands.py lua # Run only lua test
11+
python tests/metactl/test_all_subcommands.py status get # Run status and get tests
1012
"""
1113

1214
import sys
1315
import time
16+
import argparse
1417
from utils import print_title, kill_databend_meta
1518
import shutil
1619

@@ -24,6 +27,7 @@
2427
from subcommands import cmd_export_from_raft_dir
2528
from subcommands import cmd_import
2629
from subcommands import cmd_transfer_leader
30+
from subcommands import cmd_lua
2731

2832

2933
def cleanup_environment():
@@ -36,17 +40,17 @@ def cleanup_environment():
3640

3741
def main():
3842
"""Main test execution function."""
43+
parser = argparse.ArgumentParser(description="Run metactl subcommand tests")
44+
parser.add_argument("tests", nargs="*", help="Specific test names to run (default: all)")
45+
args = parser.parse_args()
3946

4047
print_title("Metactl Subcommand Test Suite")
4148

4249
passed_tests = 0
4350
failed_tests = []
4451

45-
print("📋 Executing 9 test suites sequentially...")
46-
print("=" * 60)
47-
48-
# Run each test directly
49-
test_functions = [
52+
# All available test functions
53+
all_test_functions = [
5054
("status", cmd_status.main),
5155
("upsert", cmd_upsert.main),
5256
("get", cmd_get.main),
@@ -56,8 +60,29 @@ def main():
5660
("export_from_raft_dir", cmd_export_from_raft_dir.main),
5761
("import", cmd_import.main),
5862
("transfer_leader", cmd_transfer_leader.main),
63+
("lua", cmd_lua.main),
5964
]
6065

66+
# Filter tests based on command line arguments
67+
if args.tests:
68+
# Create a mapping for easier lookup
69+
test_map = {name: func for name, func in all_test_functions}
70+
test_functions = []
71+
72+
for test_name in args.tests:
73+
if test_name in test_map:
74+
test_functions.append((test_name, test_map[test_name]))
75+
else:
76+
available_tests = ", ".join(test_map.keys())
77+
print(f"❌ Unknown test '{test_name}'. Available tests: {available_tests}")
78+
return 1
79+
else:
80+
test_functions = all_test_functions
81+
82+
total_tests = len(test_functions)
83+
print(f"📋 Executing {total_tests} test suite{'s' if total_tests != 1 else ''} sequentially...")
84+
print("=" * 60)
85+
6186
for name, test_func in test_functions:
6287
cleanup_environment()
6388

@@ -77,10 +102,10 @@ def main():
77102

78103
# Print summary
79104
print_title("Test Suite Summary")
80-
print(f"📊 Total test suites: 9")
105+
print(f"📊 Total test suites: {total_tests}")
81106
print(f"✅ Passed: {passed_tests}")
82107
print(f"❌ Failed: {len(failed_tests)}")
83-
print(f"📈 Success rate: {(passed_tests/9*100):.1f}%")
108+
print(f"📈 Success rate: {(passed_tests/total_tests*100):.1f}%")
84109

85110
if failed_tests:
86111
print(f"\n🚨 Failed test suites:")

0 commit comments

Comments
 (0)