feat(nodectl): settings mutations via REST API#91
Conversation
- Unified POST /v1/elections/settings for stake-policy, tick-interval, max-factor - Unified POST /v1/ton-http-api with append flag for set/add - POST /v1/log for log settings - Added save_to_file to elections exclude/include handlers - Removed config stake-policy alias, moved vault_secret_missing to utils - CI: moved ton-http-api set from phase 2 to phase 8
…ions Entity CRUD and ton-http-api handlers signal config_changed after save_to_file so the service loop calls force_reload to rebuild wallets, pools, RPC client and restart tasks immediately.
There was a problem hiding this comment.
Pull request overview
Migrates multiple nodectl config “settings mutation” commands (elections, log, ton-http-api, include/exclude) from direct file edits to REST API calls, and wires REST-side structural mutations to trigger cache rebuilds so changes take effect immediately.
Changes:
- Add REST mutation endpoints for elections settings, ton-http-api, and log; migrate old stake-strategy endpoint to elections settings.
- Introduce
config_changednotification path so structural REST mutations triggerRuntimeConfigStore::force_reload()and task restarts. - Consolidate mutation persistence by switching remaining
update_with/save_to_filecall sites toupdate_and_save.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/node/tests/test_run_net_py/run_singlehost_nodectl.py | Moves ton-http-api set to a phase where REST/auth is available. |
| src/node-control/service/src/service_main_task.rs | Adds config_changed Notify and force_reload + restart on structural REST mutations. |
| src/node-control/service/src/runtime_config.rs | Adds force_reload() helper; continues migration toward update_and_save. |
| src/node-control/service/src/http/http_server_task.rs | Replaces stake-strategy route with elections settings mutation route; updates tests and uses update_and_save. |
| src/node-control/service/src/http/config_handlers.rs | Implements REST mutation handlers for elections settings, ton-http-api, and log; emits config_changed for structural changes. |
| src/node-control/service/src/http/auth_tests.rs | Updates auth tests to target new elections settings mutation endpoint and adds config_changed to test state. |
| src/node-control/commands/src/commands/nodectl/utils.rs | Adds shared vault_secret_missing helper for CLI commands. |
| src/node-control/commands/src/commands/nodectl/service_api_cmd.rs | Updates stake policy mutation to call /v1/elections/settings with a mirrored request DTO. |
| src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs | Reuses shared vault helper; removes local duplicate. |
| src/node-control/commands/src/commands/nodectl/config_ton_http_api_cmd.rs | Migrates ton-http-api set/add to REST calls. |
| src/node-control/commands/src/commands/nodectl/config_node_cmd.rs | Reuses shared vault helper; removes local duplicate. |
| src/node-control/commands/src/commands/nodectl/config_log_cmd.rs | Migrates log set to REST call and renders response. |
| src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs | Migrates elections mutations (policy/tick/max-factor/include/exclude) to REST calls. |
| src/node-control/commands/src/commands/nodectl/config_cmd.rs | Removes legacy stake-policy shortcut and routes ton-http-api/log/elections mutations through REST-aware commands. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if let Err(e) = runtime_cfg.force_reload().await { | ||
| tracing::error!("cache rebuild after config mutation failed: {e:#}"); | ||
| } | ||
| for task in tasks.values() { | ||
| let _ = task.restart().await; | ||
| } |
There was a problem hiding this comment.
In the config_changed branch, tasks are restarted even when runtime_cfg.force_reload() fails. Since update_and_save mutates only the config snapshot (not the cached wallets/pools/RPC client), a failed force_reload leaves caches inconsistent with the new config; restarting tasks in that state can cause hard-to-debug runtime errors. Consider restarting tasks only after a successful force_reload, and otherwise leaving tasks running on the previous caches (and/or reverting the config change / returning an error to the caller).
| if let Err(e) = runtime_cfg.force_reload().await { | |
| tracing::error!("cache rebuild after config mutation failed: {e:#}"); | |
| } | |
| for task in tasks.values() { | |
| let _ = task.restart().await; | |
| } | |
| match runtime_cfg.force_reload().await { | |
| Ok(()) => { | |
| for task in tasks.values() { | |
| let _ = task.restart().await; | |
| } | |
| } | |
| Err(e) => { | |
| tracing::error!( | |
| "cache rebuild after config mutation failed; skipping task restart: {e:#}" | |
| ); | |
| } | |
| } |
| if append { | ||
| let existing = cfg.ton_http_api.endpoints(); | ||
| for url in &urls { | ||
| if !existing.iter().any(|e| e == url) { | ||
| let entry = match &api_key { | ||
| Some(key) => { | ||
| EndpointEntry::WithKey { url: url.clone(), api_key: key.clone() } | ||
| } | ||
| None => EndpointEntry::Url(url.clone()), | ||
| }; | ||
| cfg.ton_http_api.urls.push(entry); | ||
| } |
There was a problem hiding this comment.
In append mode, existing is computed once before the loop, so duplicates within the incoming urls list (or duplicates added earlier in the loop) can still be pushed into cfg.ton_http_api.urls. This can bloat the stored config even though endpoints() dedupes for display. Consider tracking a mutable set of seen URLs (initialized from resolved_endpoints()/endpoints()) and inserting into it as you push new entries.
| let level = req | ||
| .level | ||
| .as_deref() | ||
| .map(|l| { | ||
| tracing::Level::from_str(l) | ||
| .map_err(|_| AppError::bad_request(format!("invalid log level: '{l}'"))) | ||
| }) | ||
| .transpose()?; |
There was a problem hiding this comment.
req.level is parsed as-is; values with leading/trailing whitespace (e.g. " info ") will be rejected even though they’re semantically valid. Consider trimming the string before Level::from_str to make the API more robust.
| // Pre-validate: file/all output requires a path (check against current + incoming) | ||
| if let Some(ref output) = req.output { | ||
| if matches!(output, LogOutput::File | LogOutput::All) { | ||
| let has_path = req.path.is_some() | ||
| || state.runtime_cfg.get().log.as_ref().and_then(|l| l.path.as_ref()).is_some(); | ||
| if !has_path { | ||
| let mode = match output { |
There was a problem hiding this comment.
The "output requires a path" pre-validation treats req.path.is_some() as sufficient, but an empty string path would pass this check and later be persisted as PathBuf(""). Consider trimming/validating req.path and treating empty/whitespace-only values as unset (or returning 400).
| /// Client-side mirror of `ElectionsSettingsUpdateRequest` in `service::http::config_handlers`. | ||
| /// Must stay in sync with the server-side definition. | ||
| #[derive(Clone, Default, serde::Serialize)] | ||
| struct ElectionsSettingsRequest { | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| policy: Option<StakePolicy>, | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| node: Option<String>, | ||
| #[serde(default, skip_serializing_if = "std::ops::Not::not")] | ||
| reset: bool, | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| tick_interval: Option<u64>, | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| max_factor: Option<f32>, | ||
| } |
There was a problem hiding this comment.
There are now multiple client-side request structs mirroring the same server DTO (ElectionsSettingsUpdateRequest) across commands (e.g. here and in config_elections_cmd.rs). This is easy to let drift over time. Consider defining a single shared request type in one module (or a common crate) and reusing it for all callers of /v1/elections/settings.
There was a problem hiding this comment.
Agreed — this is real duplication. Tracking as a follow-up in v0.5.0: a refactor to extract shared REST request/response DTOs into a common module reused by both the service handlers and the CLI commands. Out of scope for this PR.
… dedupe ton-http-api, trim log inputs
…ig ton-http-api` cmd - breaking change! to use a different word from `--url` option in the config root cmd
Summary
Part 3 of centralizing config management. Moves all settings mutation commands to REST API endpoints.
POST /v1/elections/settingsreplaces/v1/stake_strategy, individual tick-interval and max-factor endpoints. Accepts any combination of policy, tick_interval, max_factor in one request.POST /v1/ton-http-apiwithappendflag replaces separate set/add endpointsPOST /v1/logfor log settings mutationsave_to_file()to elections exclude/include handlers for persistence consistencyconfig stake-policyCLI alias (useconfig elections stake-policy)Notify+force_reload()Breaking changes
config ton-http-api: renamed--urloption to--endpoint(disambiguates from the rootconfig --urloption). Update any scripts/automation invokingnodectl config ton-http-api --url ...to use--endpoint.CI script
config ton-http-api setfrom phase 2 (before service) to phase 8 (after service + auth). Service starts with default URL fromconfig generate.Notes
Closes SMA-70