feat(proxy): 实现代理节点批量升级回滚、隧道重定向跟随及远程配置管理

核心功能:
- 新增代理节点批量升级回滚工作流,支持分批升级、健康探针、跳过/重试/取消等操作
- proxy 隧道流处理器支持 HTTP 重定向跟随(最多 10 跳),区分 307/308 可重播与不可重播请求体
- proxy 协议新增 follow_redirects / http1_only 字段,网关侧同步支持
- 新增代理节点远端配置变更接口(名称、允许端口、调度状态、升级目标等)
- 新增代理节点注册/反注册/心跳的 Admin API,及节点过期清理维护任务
- gateway 隧道 owner-relay 支持流式代理大请求体,新增 5 MiB 默认限制
- 新增 ProxyNodeRegistrationMutation / ProxyNodeRemoteConfigMutation 数据类型
- proxy 配置新增重定向重播预算、心跳间隔等参数,TUI 安装向导同步更新
- 前端 ProxyNodes 页面新增批量升级操作面板及滚动进度展示
This commit is contained in:
fawney19
2026-04-12 16:02:38 +08:00
parent 7c5bb7f383
commit 9703840a36
83 changed files with 11832 additions and 1520 deletions

View File

@@ -178,6 +178,69 @@ pub(super) fn classify_admin_operations_family_route(
"admin:proxy_nodes",
false,
))
} else if method == http::Method::POST
&& matches!(
normalized_path,
"/api/admin/proxy-nodes/upgrade/cancel" | "/api/admin/proxy-nodes/upgrade/cancel/"
)
{
Some(classified(
"admin_proxy",
"proxy_nodes_manage",
"cancel_upgrade_rollout",
"admin:proxy_nodes",
false,
))
} else if method == http::Method::POST
&& matches!(
normalized_path,
"/api/admin/proxy-nodes/upgrade/clear-conflicts"
| "/api/admin/proxy-nodes/upgrade/clear-conflicts/"
)
{
Some(classified(
"admin_proxy",
"proxy_nodes_manage",
"clear_upgrade_rollout_conflicts",
"admin:proxy_nodes",
false,
))
} else if method == http::Method::POST
&& matches!(
normalized_path,
"/api/admin/proxy-nodes/upgrade/restore-skipped"
| "/api/admin/proxy-nodes/upgrade/restore-skipped/"
)
{
Some(classified(
"admin_proxy",
"proxy_nodes_manage",
"restore_skipped_upgrade_rollout_nodes",
"admin:proxy_nodes",
false,
))
} else if method == http::Method::POST
&& normalized_path.starts_with("/api/admin/proxy-nodes/")
&& normalized_path.ends_with("/upgrade/skip")
{
Some(classified(
"admin_proxy",
"proxy_nodes_manage",
"skip_upgrade_rollout_node",
"admin:proxy_nodes",
false,
))
} else if method == http::Method::POST
&& normalized_path.starts_with("/api/admin/proxy-nodes/")
&& normalized_path.ends_with("/upgrade/retry")
{
Some(classified(
"admin_proxy",
"proxy_nodes_manage",
"retry_upgrade_rollout_node",
"admin:proxy_nodes",
false,
))
} else if method == http::Method::POST
&& matches!(
normalized_path,

View File

@@ -58,3 +58,113 @@ fn classifies_admin_proxy_nodes_events_as_admin_proxy_route() {
);
assert!(!decision.is_execution_runtime_candidate());
}
#[test]
fn classifies_admin_proxy_nodes_upgrade_cancel_as_admin_proxy_route() {
let headers = headers(&[]);
let uri: Uri = "/api/admin/proxy-nodes/upgrade/cancel"
.parse()
.expect("uri should parse");
let decision =
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_class.as_deref(), Some("admin_proxy"));
assert_eq!(decision.route_family.as_deref(), Some("proxy_nodes_manage"));
assert_eq!(
decision.route_kind.as_deref(),
Some("cancel_upgrade_rollout")
);
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("admin:proxy_nodes")
);
assert!(!decision.is_execution_runtime_candidate());
}
#[test]
fn classifies_admin_proxy_nodes_upgrade_clear_conflicts_as_admin_proxy_route() {
let headers = headers(&[]);
let uri: Uri = "/api/admin/proxy-nodes/upgrade/clear-conflicts"
.parse()
.expect("uri should parse");
let decision =
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_class.as_deref(), Some("admin_proxy"));
assert_eq!(decision.route_family.as_deref(), Some("proxy_nodes_manage"));
assert_eq!(
decision.route_kind.as_deref(),
Some("clear_upgrade_rollout_conflicts")
);
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("admin:proxy_nodes")
);
assert!(!decision.is_execution_runtime_candidate());
}
#[test]
fn classifies_admin_proxy_nodes_upgrade_restore_skipped_as_admin_proxy_route() {
let headers = headers(&[]);
let uri: Uri = "/api/admin/proxy-nodes/upgrade/restore-skipped"
.parse()
.expect("uri should parse");
let decision =
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_class.as_deref(), Some("admin_proxy"));
assert_eq!(decision.route_family.as_deref(), Some("proxy_nodes_manage"));
assert_eq!(
decision.route_kind.as_deref(),
Some("restore_skipped_upgrade_rollout_nodes")
);
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("admin:proxy_nodes")
);
assert!(!decision.is_execution_runtime_candidate());
}
#[test]
fn classifies_admin_proxy_nodes_upgrade_skip_node_as_admin_proxy_route() {
let headers = headers(&[]);
let uri: Uri = "/api/admin/proxy-nodes/node-1/upgrade/skip"
.parse()
.expect("uri should parse");
let decision =
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_class.as_deref(), Some("admin_proxy"));
assert_eq!(decision.route_family.as_deref(), Some("proxy_nodes_manage"));
assert_eq!(
decision.route_kind.as_deref(),
Some("skip_upgrade_rollout_node")
);
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("admin:proxy_nodes")
);
assert!(!decision.is_execution_runtime_candidate());
}
#[test]
fn classifies_admin_proxy_nodes_upgrade_retry_node_as_admin_proxy_route() {
let headers = headers(&[]);
let uri: Uri = "/api/admin/proxy-nodes/node-1/upgrade/retry"
.parse()
.expect("uri should parse");
let decision =
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_class.as_deref(), Some("admin_proxy"));
assert_eq!(decision.route_family.as_deref(), Some("proxy_nodes_manage"));
assert_eq!(
decision.route_kind.as_deref(),
Some("retry_upgrade_rollout_node")
);
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("admin:proxy_nodes")
);
assert!(!decision.is_execution_runtime_candidate());
}