feat(nacos): 增强 NacosAdmin 接口支持命名空间列表和配置发布表单

- 添加 list_namespaces 方法,支持从 Nacos 获取命名空间列表
- 重构 publish_config 方法,使用 build_publish_forms 函数构建 V3 和 V1 的发布表单
- 增加 namespace_list_error 函数,改进命名空间列表错误处理
- 添加单元测试,验证表单构建和错误处理逻辑
This commit is contained in:
二丫讲梵 2026-06-27 11:33:07 +08:00 committed by GitHub
parent c15fb85561
commit c0bd9313d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 88 additions and 32 deletions

View File

@ -341,6 +341,7 @@ impl NacosAdmin for NacosOpenApiAdmin {
async fn test_connection(&self) -> Result<NacosConnectionInfo, String> {
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<Vec<NacosNamespaceInfo>, 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<St
}
}
fn build_publish_forms(req: NacosConfigUpsert, namespace: String) -> (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<Value, String> {
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");
}