From c0bd9313d730f61dc09667d7ce24be64c282b59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Sat, 27 Jun 2026 11:33:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(nacos):=20=E5=A2=9E=E5=BC=BA=20NacosAdmin?= =?UTF-8?q?=20=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E5=88=97=E8=A1=A8=E5=92=8C=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 list_namespaces 方法,支持从 Nacos 获取命名空间列表 - 重构 publish_config 方法,使用 build_publish_forms 函数构建 V3 和 V1 的发布表单 - 增加 namespace_list_error 函数,改进命名空间列表错误处理 - 添加单元测试,验证表单构建和错误处理逻辑 --- crates/dbx-core/src/nacos/http.rs | 120 ++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 32 deletions(-) diff --git a/crates/dbx-core/src/nacos/http.rs b/crates/dbx-core/src/nacos/http.rs index 53f24b9ab..689c7cb44 100644 --- a/crates/dbx-core/src/nacos/http.rs +++ b/crates/dbx-core/src/nacos/http.rs @@ -341,6 +341,7 @@ impl NacosAdmin for NacosOpenApiAdmin { async fn test_connection(&self) -> Result { let raw = self.get_server_state().await?; let _ = self.access_token().await?; + let _ = self.list_namespaces().await?; Ok(NacosConnectionInfo { server_addr: self.cfg.server_addr.clone(), display_server_addr: self.cfg.display_server_addr.clone(), @@ -358,7 +359,10 @@ impl NacosAdmin for NacosOpenApiAdmin { async fn list_namespaces(&self) -> Result, String> { let value = match self.get_json("/v3/console/core/namespace/list", Vec::new()).await { Ok(value) => value, - Err(_) => self.get_json("/v1/console/namespaces", Vec::new()).await?, + Err(v3_err) => self + .get_json("/v1/console/namespaces", Vec::new()) + .await + .map_err(|v1_err| namespace_list_error(&v3_err, &v1_err))?, }; Ok(parse_namespaces(value)) } @@ -524,35 +528,15 @@ impl NacosAdmin for NacosOpenApiAdmin { async fn publish_config(&self, req: NacosConfigUpsert) -> Result<(), String> { let namespace = self.namespace(req.namespace.as_deref()); - let mut v3_query = vec![ - ("dataId".to_string(), req.data_id.clone()), - ("groupName".to_string(), req.group.clone()), - ("content".to_string(), req.content.clone()), - ("namespaceId".to_string(), namespace.clone()), - ]; - push_optional(&mut v3_query, "type", req.config_type.clone()); - push_optional(&mut v3_query, "appName", req.app_name.clone()); - push_optional(&mut v3_query, "desc", req.desc.clone()); - push_optional(&mut v3_query, "tags", req.tags.clone()); - - let mut v1_form = vec![ - ("dataId".to_string(), req.data_id), - ("group".to_string(), req.group), - ("content".to_string(), req.content), - ("tenant".to_string(), namespace), - ]; - push_optional(&mut v1_form, "type", req.config_type); - push_optional(&mut v1_form, "appName", req.app_name); - push_optional(&mut v1_form, "desc", req.desc); - push_optional(&mut v1_form, "config_tags", req.tags); + let (v3_form, v1_form) = build_publish_forms(req, namespace); let mut errors = Vec::new(); - for (path, query) in [ - ("/v3/console/cs/config", v3_query.clone()), - ("/v3/console/cs/config/publish", v3_query.clone()), - ("/v3/console/cs/config/update", v3_query), + for (path, form) in [ + ("/v3/console/cs/config", v3_form.clone()), + ("/v3/console/cs/config/publish", v3_form.clone()), + ("/v3/console/cs/config/update", v3_form), ] { - match self.request(reqwest::Method::POST, path, query, None, None).await { + match self.request(reqwest::Method::POST, path, Vec::new(), Some(form), None).await { Ok(resp) => match error_for_status(resp, path).await { Ok(_) => return Ok(()), Err(err) => errors.push(err), @@ -988,6 +972,38 @@ fn push_optional(params: &mut Vec<(String, String)>, key: &str, value: Option (Vec<(String, String)>, Vec<(String, String)>) { + let mut v3_form = vec![ + ("dataId".to_string(), req.data_id.clone()), + ("groupName".to_string(), req.group.clone()), + ("content".to_string(), req.content.clone()), + ("namespaceId".to_string(), namespace.clone()), + ]; + push_optional(&mut v3_form, "type", req.config_type.clone()); + push_optional(&mut v3_form, "appName", req.app_name.clone()); + push_optional(&mut v3_form, "desc", req.desc.clone()); + push_optional(&mut v3_form, "configTags", req.tags.clone()); + push_optional(&mut v3_form, "config_tags", req.tags.clone()); + + let mut v1_form = vec![ + ("dataId".to_string(), req.data_id), + ("group".to_string(), req.group), + ("content".to_string(), req.content), + ("tenant".to_string(), namespace), + ]; + push_optional(&mut v1_form, "type", req.config_type); + push_optional(&mut v1_form, "appName", req.app_name); + push_optional(&mut v1_form, "desc", req.desc); + push_optional(&mut v1_form, "config_tags", req.tags); + + (v3_form, v1_form) +} + +fn namespace_list_error(v3_err: &str, v1_err: &str) -> String { + let message = format!("Failed to list Nacos namespaces with v3 and v1 APIs. v3: {v3_err}; v1: {v1_err}"); + classified_error(classify_nacos_error(&message), &message) +} + async fn response_json_or_text(resp: reqwest::Response) -> Result { let bytes = resp.bytes().await.map_err(|e| format!("Failed to read Nacos response: {e}"))?; if bytes.is_empty() { @@ -1023,11 +1039,7 @@ fn classify_nacos_error(message: &str) -> &'static str { { return "authFailed"; } - if lower.contains("no static resource") - || lower.contains("context path") - || lower.contains("path\":\"/") - || lower.contains("path=/") - { + if lower.contains("no static resource") || lower.contains("context path") { return "contextPathMismatch"; } if lower.contains("history") @@ -1476,6 +1488,44 @@ mod tests { assert_eq!(parsed.content.as_deref(), Some("hello")); } + #[test] + fn builds_v3_publish_form_fields() { + let (v3_form, v1_form) = build_publish_forms( + NacosConfigUpsert { + namespace: Some("ops".to_string()), + data_id: "app.yaml".to_string(), + group: "DEFAULT_GROUP".to_string(), + content: "server:\n port: 8080".to_string(), + config_type: Some("yaml".to_string()), + app_name: Some("portal".to_string()), + desc: Some("main config".to_string()), + tags: Some("prod,gray".to_string()), + }, + "ops".to_string(), + ); + + assert!(v3_form.contains(&("dataId".to_string(), "app.yaml".to_string()))); + assert!(v3_form.contains(&("groupName".to_string(), "DEFAULT_GROUP".to_string()))); + assert!(v3_form.contains(&("namespaceId".to_string(), "ops".to_string()))); + assert!(v3_form.contains(&("content".to_string(), "server:\n port: 8080".to_string()))); + assert!(v3_form.contains(&("type".to_string(), "yaml".to_string()))); + assert!(v3_form.contains(&("configTags".to_string(), "prod,gray".to_string()))); + assert!(v3_form.contains(&("config_tags".to_string(), "prod,gray".to_string()))); + assert!(v1_form.contains(&("group".to_string(), "DEFAULT_GROUP".to_string()))); + assert!(v1_form.contains(&("tenant".to_string(), "ops".to_string()))); + } + + #[test] + fn namespace_list_error_keeps_v3_and_v1_details() { + let err = namespace_list_error( + "NACOS_ERROR[authFailed]: Nacos admin /v3/console/core/namespace/list returned 403 Forbidden", + "NACOS_ERROR[apiVersionMismatch]: Nacos admin /v1/console/namespaces returned 410 Gone", + ); + assert!(err.starts_with("NACOS_ERROR[authFailed]:")); + assert!(err.contains("/v3/console/core/namespace/list returned 403 Forbidden")); + assert!(err.contains("/v1/console/namespaces returned 410 Gone")); + } + #[test] fn parses_v1_show_all_config_detail_metadata() { let parsed = parse_config_detail( @@ -1696,6 +1746,12 @@ mod tests { fn classifies_common_nacos_errors() { assert_eq!(classify_nacos_error("401 Unauthorized invalid access token"), "authFailed"); assert_eq!(classify_nacos_error("No static resource nacos/v3/console/server/state"), "contextPathMismatch"); + assert_eq!( + classify_nacos_error( + r#"410 Gone {"message":"Current API will be deprecated","path":"/v1/console/namespaces"}"# + ), + "apiVersionMismatch" + ); assert_eq!(classify_nacos_error("404 Not Found"), "apiVersionMismatch"); assert_eq!(classify_nacos_error("connection refused"), "connectionFailed"); }