Skip to content

Commit 911bd7a

Browse files
authored
feat: add BE configuration update tool (#23)
1 parent db57617 commit 911bd7a

File tree

9 files changed

+254
-81
lines changed

9 files changed

+254
-81
lines changed

src/tools/be/be_config_update.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use super::be_http_client;
2+
use crate::config::Config;
3+
use crate::error::{CliError, Result};
4+
use crate::tools::ExecutionResult;
5+
use crate::tools::Tool;
6+
use crate::ui;
7+
use dialoguer::{Confirm, Input, theme::ColorfulTheme};
8+
use serde::Deserialize;
9+
use std::path::PathBuf;
10+
11+
#[derive(Deserialize)]
12+
struct ConfigUpdateResult {
13+
config_name: String,
14+
status: String,
15+
msg: String,
16+
}
17+
18+
pub struct BeUpdateConfigTool;
19+
20+
impl Tool for BeUpdateConfigTool {
21+
fn name(&self) -> &str {
22+
"set-be-config"
23+
}
24+
25+
fn description(&self) -> &str {
26+
"Update BE configuration variables"
27+
}
28+
29+
fn execute(&self, _config: &Config, _pid: u32) -> Result<ExecutionResult> {
30+
let key = prompt_input("Enter BE config key to update")?;
31+
let value = prompt_input(&format!("Enter value for '{key}'"))?;
32+
let persist = Confirm::with_theme(&ColorfulTheme::default())
33+
.with_prompt("Persist this configuration?")
34+
.default(false)
35+
.interact()
36+
.map_err(|e| CliError::InvalidInput(format!("Input failed: {e}")))?;
37+
38+
ui::print_info(&format!(
39+
"Updating BE config: {key}={value} (persist: {persist})"
40+
));
41+
42+
let endpoint = format!("/api/update_config?{key}={value}&persist={persist}");
43+
handle_update_result(be_http_client::post_be_endpoint(&endpoint), &key)
44+
}
45+
46+
fn requires_pid(&self) -> bool {
47+
false
48+
}
49+
}
50+
51+
fn prompt_input(prompt: &str) -> Result<String> {
52+
let input: String = Input::with_theme(&ColorfulTheme::default())
53+
.with_prompt(prompt)
54+
.interact_text()
55+
.map_err(|e| CliError::InvalidInput(format!("Input failed: {e}")))?;
56+
57+
let trimmed = input.trim();
58+
if trimmed.is_empty() {
59+
ui::print_warning("Input cannot be empty!");
60+
Err(CliError::GracefulExit)
61+
} else {
62+
Ok(trimmed.to_string())
63+
}
64+
}
65+
66+
fn get_current_value(key: &str) -> Option<String> {
67+
be_http_client::request_be_webserver_port("/varz", Some(key))
68+
.ok()?
69+
.lines()
70+
.next()?
71+
.split('=')
72+
.nth(1)
73+
.map(|v| v.trim().to_string())
74+
}
75+
76+
fn handle_update_result(result: Result<String>, key: &str) -> Result<ExecutionResult> {
77+
let json_response = result.map_err(|e| {
78+
ui::print_error(&format!("Failed to update BE config: {e}."));
79+
ui::print_info("Tips: Ensure the BE service is running and accessible.");
80+
e
81+
})?;
82+
83+
let results: Vec<ConfigUpdateResult> = serde_json::from_str(&json_response)
84+
.map_err(|e| CliError::ToolExecutionFailed(format!("Failed to parse response: {e}")))?;
85+
86+
println!();
87+
ui::print_info("Results:");
88+
89+
let all_ok = results.iter().all(|item| {
90+
if item.status == "OK" {
91+
match get_current_value(&item.config_name) {
92+
Some(value) => println!(" ✓ {} = {}", item.config_name, value),
93+
None => println!(" ✓ {}: OK", item.config_name),
94+
}
95+
true
96+
} else {
97+
println!(" ✗ {}: FAILED - {}", item.config_name, item.msg);
98+
false
99+
}
100+
});
101+
102+
if all_ok {
103+
Ok(ExecutionResult {
104+
output_path: PathBuf::from("console_output"),
105+
message: format!("Config '{key}' updated successfully"),
106+
})
107+
} else {
108+
Err(CliError::ToolExecutionFailed(
109+
"Some configurations failed to update".to_string(),
110+
))
111+
}
112+
}

src/tools/be/be_http_client.rs

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,30 @@ use std::process::Command;
88

99
const BE_DEFAULT_IP: &str = "127.0.0.1";
1010

11-
/// Send an HTTP GET request to a BE API endpoint
12-
pub fn request_be_webserver_port(endpoint: &str, filter_pattern: Option<&str>) -> Result<String> {
13-
let mut be_targets: BTreeSet<(String, u16)> = BTreeSet::new();
14-
11+
fn get_be_targets() -> Result<BTreeSet<(String, u16)>> {
1512
let ports = get_be_http_ports()?;
16-
1713
let selected_host = be::list::get_selected_be_host();
18-
1914
let cluster_hosts = get_be_ip().unwrap_or_default();
2015

2116
let mut all_hosts = BTreeSet::new();
22-
if let Some(host) = &selected_host {
23-
all_hosts.insert(host.clone());
24-
}
25-
for host in cluster_hosts {
17+
if let Some(host) = selected_host {
2618
all_hosts.insert(host);
2719
}
20+
all_hosts.extend(cluster_hosts);
2821

2922
if all_hosts.is_empty() {
3023
all_hosts.insert(BE_DEFAULT_IP.to_string());
3124
}
3225

33-
for host in all_hosts {
34-
be_targets.extend(ports.iter().map(|p| (host.clone(), *p)));
35-
}
26+
Ok(all_hosts
27+
.into_iter()
28+
.flat_map(|host| ports.iter().map(move |p| (host.clone(), *p)))
29+
.collect())
30+
}
31+
32+
/// Send an HTTP GET request to a BE API endpoint
33+
pub fn request_be_webserver_port(endpoint: &str, filter_pattern: Option<&str>) -> Result<String> {
34+
let be_targets = get_be_targets()?;
3635

3736
for (host, port) in &be_targets {
3837
let url = format!("http://{host}:{port}{endpoint}");
@@ -104,3 +103,31 @@ pub fn get_be_ip() -> Result<Vec<String>> {
104103

105104
Ok(vec![BE_DEFAULT_IP.to_string()])
106105
}
106+
107+
/// Send an HTTP POST request to a BE API endpoint
108+
pub fn post_be_endpoint(endpoint: &str) -> Result<String> {
109+
let be_targets = get_be_targets()?;
110+
111+
for (host, port) in &be_targets {
112+
let url = format!("http://{host}:{port}{endpoint}");
113+
let mut curl_cmd = Command::new("curl");
114+
curl_cmd.args(["-sS", "-X", "POST", &url]);
115+
116+
if let Ok(output) = executor::execute_command(&mut curl_cmd, "curl") {
117+
return Ok(String::from_utf8_lossy(&output.stdout).to_string());
118+
}
119+
}
120+
121+
let ports_str = be_targets
122+
.iter()
123+
.map(|(h, p)| format!("{h}:{p}"))
124+
.collect::<Vec<_>>()
125+
.join(", ");
126+
127+
ui::print_warning(
128+
"Could not connect to any BE http endpoint. You can select a host via 'be-list'.",
129+
);
130+
Err(CliError::ToolExecutionFailed(format!(
131+
"Could not connect to any BE http port ({ports_str}). Check if BE is running."
132+
)))
133+
}

src/tools/be/be_vars.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ impl Tool for BeVarsTool {
3131
let result = be_http_client::request_be_webserver_port("/varz", Some(&variable_name));
3232

3333
let handler = BeResponseHandler {
34-
success_message: "Query completed!",
3534
empty_warning: "No variables found matching '{}'.",
3635
error_context: "Failed to query BE",
3736
tips: "Ensure the BE service is running and accessible.",

src/tools/be/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod be_config_update;
12
mod be_http_client;
23
mod be_vars;
34
mod jmap;
@@ -7,6 +8,7 @@ mod pipeline_tasks;
78
mod pstack;
89
mod response_handler;
910

11+
pub use be_config_update::BeUpdateConfigTool;
1012
pub use be_vars::BeVarsTool;
1113
pub use jmap::{JmapDumpTool, JmapHistoTool};
1214
pub use list::BeListTool;

src/tools/be/pipeline_tasks.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ impl Tool for PipelineTasksTool {
2323
let result = be_http_client::request_be_webserver_port("/api/running_pipeline_tasks", None);
2424

2525
let handler = BeResponseHandler {
26-
success_message: "Pipeline tasks fetched successfully!",
2726
empty_warning: "No running pipeline tasks found.",
2827
error_context: "Failed to fetch pipeline tasks",
2928
tips: "Ensure the BE service is running and accessible.",

src/tools/be/response_handler.rs

Lines changed: 42 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,39 @@ use std::path::PathBuf;
88

99
/// Configuration for handling BE API responses
1010
pub struct BeResponseHandler<'a> {
11-
pub success_message: &'a str,
1211
pub empty_warning: &'a str,
1312
pub error_context: &'a str,
1413
pub tips: &'a str,
1514
}
1615

1716
impl<'a> BeResponseHandler<'a> {
17+
fn handle_error(&self, e: crate::error::CliError) -> crate::error::CliError {
18+
ui::print_error(&format!("{}: {e}.", self.error_context));
19+
ui::print_info(&format!("Tips: {}", self.tips));
20+
e
21+
}
22+
1823
/// Handle response for console-only output (like be_vars)
1924
pub fn handle_console_result(
2025
&self,
2126
result: Result<String>,
2227
context: &str,
2328
) -> Result<ExecutionResult> {
24-
match result {
25-
Ok(output) => {
26-
ui::print_success(self.success_message);
27-
println!();
28-
ui::print_info("Results:");
29+
let output = result.map_err(|e| self.handle_error(e))?;
2930

30-
if output.is_empty() {
31-
ui::print_warning(&self.empty_warning.replace("{}", context));
32-
} else {
33-
println!("{output}");
34-
}
31+
println!();
32+
ui::print_info("Results:");
3533

36-
Ok(ExecutionResult {
37-
output_path: PathBuf::from("console_output"),
38-
message: format!("Query completed for: {context}"),
39-
})
40-
}
41-
Err(e) => {
42-
ui::print_error(&format!("{}: {e}.", self.error_context));
43-
ui::print_info(&format!("Tips: {}", self.tips));
44-
Err(e)
45-
}
34+
if output.is_empty() {
35+
ui::print_warning(&self.empty_warning.replace("{}", context));
36+
} else {
37+
println!("{output}");
4638
}
39+
40+
Ok(ExecutionResult {
41+
output_path: PathBuf::from("console_output"),
42+
message: format!("Query completed for: {context}"),
43+
})
4744
}
4845

4946
/// Handle response with file output (like pipeline_tasks)
@@ -57,47 +54,36 @@ impl<'a> BeResponseHandler<'a> {
5754
where
5855
F: Fn(&str) -> String,
5956
{
60-
match result {
61-
Ok(output) => {
62-
ui::print_success(self.success_message);
63-
println!();
64-
ui::print_info("Results:");
57+
let output = result.map_err(|e| self.handle_error(e))?;
6558

66-
if output.trim().is_empty() {
67-
ui::print_warning(self.empty_warning);
59+
println!();
60+
ui::print_info("Results:");
6861

69-
Ok(ExecutionResult {
70-
output_path: PathBuf::from("console_output"),
71-
message: "No data found".to_string(),
72-
})
73-
} else {
74-
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
75-
76-
let filename = format!("{file_prefix}_{timestamp}.txt");
77-
let output_path = config.output_dir.join(filename);
62+
if output.trim().is_empty() {
63+
ui::print_warning(self.empty_warning);
64+
return Ok(ExecutionResult {
65+
output_path: PathBuf::from("console_output"),
66+
message: "No data found".to_string(),
67+
});
68+
}
7869

79-
fs::write(&output_path, &output)?;
70+
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
71+
let filename = format!("{file_prefix}_{timestamp}.txt");
72+
let output_path = config.output_dir.join(filename);
8073

81-
println!("{}", summary_fn(&output));
74+
fs::write(&output_path, &output)?;
75+
println!("{}", summary_fn(&output));
8276

83-
let message = format!(
84-
"{} saved to {}",
85-
file_prefix.replace('_', " ").to_title_case(),
86-
output_path.display()
87-
);
77+
let message = format!(
78+
"{} saved to {}",
79+
file_prefix.replace('_', " ").to_title_case(),
80+
output_path.display()
81+
);
8882

89-
Ok(ExecutionResult {
90-
output_path,
91-
message,
92-
})
93-
}
94-
}
95-
Err(e) => {
96-
ui::print_error(&format!("{}: {e}.", self.error_context));
97-
ui::print_info(&format!("Tips: {}", self.tips));
98-
Err(e)
99-
}
100-
}
83+
Ok(ExecutionResult {
84+
output_path,
85+
message,
86+
})
10187
}
10288
}
10389

src/tools/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ impl ToolRegistry {
4444
/// Creates a new tool registry with all available tools
4545
pub fn new() -> Self {
4646
use crate::tools::be::{
47-
BeListTool, BeVarsTool, MemzGlobalTool, MemzTool, PipelineTasksTool, PstackTool,
47+
BeListTool, BeUpdateConfigTool, BeVarsTool, MemzGlobalTool, MemzTool,
48+
PipelineTasksTool, PstackTool,
4849
};
4950
use crate::tools::be::{JmapDumpTool as BeJmapDumpTool, JmapHistoTool as BeJmapHistoTool};
5051
use crate::tools::fe::routine_load::get_routine_load_tools;
@@ -71,6 +72,7 @@ impl ToolRegistry {
7172
registry.be_tools.push(Box::new(BeListTool));
7273
registry.be_tools.push(Box::new(PstackTool));
7374
registry.be_tools.push(Box::new(BeVarsTool));
75+
registry.be_tools.push(Box::new(BeUpdateConfigTool));
7476
registry.be_tools.push(Box::new(BeJmapDumpTool));
7577
registry.be_tools.push(Box::new(BeJmapHistoTool));
7678
registry.be_tools.push(Box::new(PipelineTasksTool));

0 commit comments

Comments
 (0)