Files
MyGoNavi/internal/app/methods_nacos.go
Syngnat e5e80cbbe8 🐛 fix(nacos): 完善多版本连接与配置服务可靠性
- 支持 Nacos v1、v2、v3 就绪探测与受限账号连接
- 强化认证、缓存、单飞连接和关闭代际的并发边界
- 修复监听重复事件、导入身份碰撞及部分失败误报成功
- 补齐只读保护、结构化错误与六语言提示
2026-07-28 23:51:17 +08:00

1018 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/logger"
"GoNavi-Wails/internal/nacos"
"golang.org/x/sync/singleflight"
)
var (
nacosCache = make(map[string]nacos.Client)
nacosCacheMu sync.Mutex
nacosCacheGeneration uint64
nacosCacheGenerationCtx context.Context
nacosCacheGenerationCancel context.CancelFunc
nacosConnectGroup singleflight.Group
newNacosClientFunc = nacos.NewClient
)
var errNacosCacheInvalidated = errors.New("Nacos 连接缓存已关闭")
const defaultNacosOperationTimeoutSeconds = 30
const nacosNamespaceListForbiddenErrorCode = "nacos_namespace_list_forbidden"
func init() {
nacosCacheGenerationCtx, nacosCacheGenerationCancel = context.WithCancel(context.Background())
}
// NacosConfigQuery is the frontend search payload.
type NacosConfigQuery struct {
NamespaceID string `json:"namespaceId"`
DataID string `json:"dataId,omitempty"`
Group string `json:"group,omitempty"`
AppName string `json:"appName,omitempty"`
PageNo int `json:"pageNo,omitempty"`
PageSize int `json:"pageSize,omitempty"`
Search string `json:"search,omitempty"`
}
// NacosPublishConfigPayload is the frontend publish payload.
type NacosPublishConfigPayload struct {
NamespaceID string `json:"namespaceId"`
DataID string `json:"dataId"`
Group string `json:"group"`
Content string `json:"content"`
Type string `json:"type,omitempty"`
AppName string `json:"appName,omitempty"`
Desc string `json:"desc,omitempty"`
BetaIPs string `json:"betaIps,omitempty"`
}
// NacosConfigIdentity identifies one config by dataId + group.
// Named type (not anonymous) so wailsjs models.ts generation stays valid.
type NacosConfigIdentity struct {
DataID string `json:"dataId"`
Group string `json:"group"`
Index *int `json:"index,omitempty"`
}
// NacosExportConfigsOptions controls config export.
type NacosExportConfigsOptions struct {
NamespaceID string `json:"namespaceId"`
NamespaceName string `json:"namespaceName,omitempty"`
Scope string `json:"scope,omitempty"` // all | selected
Items []NacosConfigIdentity `json:"items,omitempty"`
}
// NacosImportConfigsOptions controls config import.
type NacosImportConfigsOptions struct {
NamespaceID string `json:"namespaceId"`
ConflictMode string `json:"conflictMode,omitempty"` // skip | overwrite
File string `json:"file,omitempty"`
Scope string `json:"scope,omitempty"` // all | selected
Items []NacosConfigIdentity `json:"items,omitempty"`
}
// NacosCreateNamespacePayload creates a namespace.
type NacosCreateNamespacePayload struct {
ID string `json:"id"`
ShowName string `json:"showName"`
Description string `json:"description,omitempty"`
}
// NacosUpdateNamespacePayload updates a namespace.
type NacosUpdateNamespacePayload struct {
ID string `json:"id"`
ShowName string `json:"showName"`
Description string `json:"description,omitempty"`
}
// NacosHistoryQuery lists config history.
type NacosHistoryQuery struct {
NamespaceID string `json:"namespaceId"`
DataID string `json:"dataId"`
Group string `json:"group"`
PageNo int `json:"pageNo,omitempty"`
PageSize int `json:"pageSize,omitempty"`
}
// NacosServiceQuery lists services.
type NacosServiceQuery struct {
NamespaceID string `json:"namespaceId"`
GroupName string `json:"groupName,omitempty"`
PageNo int `json:"pageNo,omitempty"`
PageSize int `json:"pageSize,omitempty"`
}
// NacosServicePayload creates/updates a service.
type NacosServicePayload struct {
NamespaceID string `json:"namespaceId"`
ServiceName string `json:"serviceName"`
GroupName string `json:"groupName,omitempty"`
Ephemeral *bool `json:"ephemeral,omitempty"`
ProtectThreshold float64 `json:"protectThreshold,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// NacosInstanceQuery lists instances.
type NacosInstanceQuery struct {
NamespaceID string `json:"namespaceId"`
ServiceName string `json:"serviceName"`
GroupName string `json:"groupName,omitempty"`
Clusters string `json:"clusters,omitempty"`
HealthyOnly bool `json:"healthyOnly,omitempty"`
}
// NacosInstancePayload mutates an instance.
type NacosInstancePayload struct {
NamespaceID string `json:"namespaceId"`
ServiceName string `json:"serviceName"`
GroupName string `json:"groupName,omitempty"`
IP string `json:"ip"`
Port int `json:"port"`
ClusterName string `json:"clusterName,omitempty"`
Weight *float64 `json:"weight,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Healthy *bool `json:"healthy,omitempty"`
Ephemeral *bool `json:"ephemeral,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
func formatNacosConnSummary(config connection.ConnectionConfig) string {
var b strings.Builder
b.WriteString("类型=nacos 地址=")
b.WriteString(strings.TrimSpace(config.Host))
b.WriteString(":")
b.WriteString(strconv.Itoa(config.Port))
if config.UseSSL {
b.WriteString(" SSL=on")
}
if user := strings.TrimSpace(config.User); user != "" {
b.WriteString(" 用户=")
b.WriteString(user)
}
if contextPath := nacosContextPathForSummary(config.ConnectionParams); contextPath != "" {
b.WriteString(" contextPath=")
b.WriteString(contextPath)
}
return b.String()
}
func nacosContextPathForSummary(raw string) string {
normalized := strings.NewReplacer(";", "&", "\r", "&", "\n", "&").Replace(raw)
values, _ := url.ParseQuery(normalized)
contextPath := strings.TrimSpace(values.Get("contextPath"))
if contextPath == "" {
return ""
}
for _, char := range contextPath {
if char < 0x20 || char == 0x7f {
return ""
}
}
if contextPath == "/" {
return "/"
}
if !strings.HasPrefix(contextPath, "/") {
contextPath = "/" + contextPath
}
return strings.TrimRight(contextPath, "/")
}
func getNacosClientCacheKey(config connection.ConnectionConfig) string {
normalized := normalizeCacheKeyConfig(config)
identity := struct {
Type string `json:"type"`
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
UseSSL bool `json:"useSSL"`
SSLMode string `json:"sslMode"`
SSLCAPath string `json:"sslCAPath"`
SSLCertPath string `json:"sslCertPath"`
SSLKeyPath string `json:"sslKeyPath"`
ConnectionParams string `json:"connectionParams"`
Database string `json:"database"`
UseSSH bool `json:"useSSH"`
SSHHost string `json:"sshHost"`
SSHPort int `json:"sshPort"`
SSHUser string `json:"sshUser"`
SSHPassword string `json:"sshPassword"`
SSHKeyPath string `json:"sshKeyPath"`
UseProxy bool `json:"useProxy"`
ProxyType string `json:"proxyType"`
ProxyHost string `json:"proxyHost"`
ProxyPort int `json:"proxyPort"`
ProxyUser string `json:"proxyUser"`
ProxyPassword string `json:"proxyPassword"`
UseHTTPTunnel bool `json:"useHttpTunnel"`
HTTPTunnelHost string `json:"httpTunnelHost"`
HTTPTunnelPort int `json:"httpTunnelPort"`
HTTPTunnelUser string `json:"httpTunnelUser"`
HTTPTunnelPassword string `json:"httpTunnelPassword"`
}{
Type: "nacos",
Host: strings.TrimSpace(normalized.Host),
Port: normalized.Port,
User: strings.TrimSpace(normalized.User),
Password: normalized.Password,
UseSSL: normalized.UseSSL,
SSLMode: strings.TrimSpace(normalized.SSLMode),
SSLCAPath: strings.TrimSpace(normalized.SSLCAPath),
SSLCertPath: strings.TrimSpace(normalized.SSLCertPath),
SSLKeyPath: strings.TrimSpace(normalized.SSLKeyPath),
ConnectionParams: strings.TrimSpace(normalized.ConnectionParams),
Database: strings.TrimSpace(normalized.Database),
UseSSH: normalized.UseSSH,
SSHHost: strings.TrimSpace(normalized.SSH.Host),
SSHPort: normalized.SSH.Port,
SSHUser: strings.TrimSpace(normalized.SSH.User),
SSHPassword: normalized.SSH.Password,
SSHKeyPath: strings.TrimSpace(normalized.SSH.KeyPath),
UseProxy: normalized.UseProxy,
ProxyType: strings.TrimSpace(normalized.Proxy.Type),
ProxyHost: strings.TrimSpace(normalized.Proxy.Host),
ProxyPort: normalized.Proxy.Port,
ProxyUser: strings.TrimSpace(normalized.Proxy.User),
ProxyPassword: normalized.Proxy.Password,
UseHTTPTunnel: normalized.UseHTTPTunnel,
HTTPTunnelHost: strings.TrimSpace(normalized.HTTPTunnel.Host),
HTTPTunnelPort: normalized.HTTPTunnel.Port,
HTTPTunnelUser: strings.TrimSpace(normalized.HTTPTunnel.User),
HTTPTunnelPassword: normalized.HTTPTunnel.Password,
}
raw, _ := json.Marshal(identity)
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
func (a *App) getNacosClient(config connection.ConnectionConfig) (nacos.Client, error) {
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
return a.getNacosClientWithContext(ctx, config)
}
func (a *App) getNacosClientWithContext(ctx context.Context, config connection.ConnectionConfig) (nacos.Client, error) {
if ctx == nil {
return nil, fmt.Errorf("Nacos 连接上下文不能为空")
}
nacosCacheMu.Lock()
requestGeneration := nacosCacheGeneration
requestGenerationCtx := nacosCacheGenerationCtx
nacosCacheMu.Unlock()
if err := ctx.Err(); err != nil {
return nil, err
}
resolvedConfig, err := a.resolveConnectionSecrets(config)
if err != nil {
wrapped := wrapConnectError(config, err)
logger.Error(wrapped, "Nacos 密文解析失败:%s", formatNacosConnSummary(config))
return nil, wrapped
}
connectConfig, proxyErr := resolveDialConfigWithProxyFunc(resolvedConfig)
if proxyErr != nil {
wrapped := wrapConnectError(resolvedConfig, proxyErr)
logger.Error(wrapped, "Nacos 代理准备失败:%s", formatNacosConnSummary(resolvedConfig))
return nil, wrapped
}
connectConfig.Type = "nacos"
if err := ctx.Err(); err != nil {
return nil, err
}
cacheIdentityConfig := resolvedConfig
cacheIdentityConfig.Type = "nacos"
key := getNacosClientCacheKey(cacheIdentityConfig)
flightKey := strconv.FormatUint(requestGeneration, 10) + ":" + key + ":" +
strconv.Itoa(nacosOperationTimeoutSeconds(connectConfig))
resultCh := nacosConnectGroup.DoChan(flightKey, func() (any, error) {
if requestGenerationCtx.Err() != nil {
return nil, errNacosCacheInvalidated
}
nacosCacheMu.Lock()
if nacosCacheGeneration != requestGeneration {
nacosCacheMu.Unlock()
return nil, errNacosCacheInvalidated
}
cachedClient := nacosCache[key]
nacosCacheMu.Unlock()
if cachedClient != nil {
// net/http transports reconnect on demand. Returning the published
// client directly also prevents timeout-specific flights from racing
// to evict and close the same cached client.
return cachedClient, nil
}
// Another cache publisher may have won after this timeout-specific cold
// connection flight started. Recheck before opening a physical client.
nacosCacheMu.Lock()
if nacosCacheGeneration != requestGeneration {
nacosCacheMu.Unlock()
return nil, errNacosCacheInvalidated
}
cachedClient = nacosCache[key]
nacosCacheMu.Unlock()
if cachedClient != nil {
return cachedClient, nil
}
if requestGenerationCtx.Err() != nil {
return nil, errNacosCacheInvalidated
}
client := newNacosClientFunc()
if err := client.Connect(connectConfig); err != nil {
_ = client.Close()
wrapped := wrapConnectError(connectConfig, err)
logger.Error(wrapped, "Nacos 连接失败:%s", formatNacosConnSummary(connectConfig))
return nil, wrapped
}
if requestGenerationCtx.Err() != nil {
_ = client.Close()
return nil, errNacosCacheInvalidated
}
nacosCacheMu.Lock()
cacheInvalidated := nacosCacheGeneration != requestGeneration
if !cacheInvalidated {
cachedClient = nacosCache[key]
}
if !cacheInvalidated && cachedClient == nil {
nacosCache[key] = client
}
nacosCacheMu.Unlock()
if cacheInvalidated {
_ = client.Close()
return nil, errNacosCacheInvalidated
}
if cachedClient != nil {
// Defensive loser cleanup: a cache writer outside this keyed flight
// must never leave an unpublished physical client alive.
_ = client.Close()
return cachedClient, nil
}
logger.Infof("Nacos 连接成功并写入缓存:%s", formatNacosConnSummary(connectConfig))
return client, nil
})
var result singleflight.Result
select {
case <-ctx.Done():
return nil, ctx.Err()
case result = <-resultCh:
}
if result.Err != nil {
return nil, result.Err
}
client, ok := result.Val.(nacos.Client)
if !ok || client == nil {
return nil, fmt.Errorf("Nacos 连接缓存返回了无效实例")
}
return client, nil
}
func (a *App) openNacosClientIsolated(config connection.ConnectionConfig) (nacos.Client, error) {
resolvedConfig, err := a.resolveConnectionSecrets(config)
if err != nil {
wrapped := wrapConnectError(config, err)
logger.Error(wrapped, "Nacos 密文解析失败:%s", formatNacosConnSummary(config))
return nil, wrapped
}
connectConfig, proxyErr := resolveDialConfigWithProxyFunc(resolvedConfig)
if proxyErr != nil {
wrapped := wrapConnectError(resolvedConfig, proxyErr)
logger.Error(wrapped, "Nacos 代理准备失败:%s", formatNacosConnSummary(resolvedConfig))
return nil, wrapped
}
connectConfig.Type = "nacos"
client := newNacosClientFunc()
if err := client.Connect(connectConfig); err != nil {
_ = client.Close()
wrapped := wrapConnectError(connectConfig, err)
logger.Error(wrapped, "Nacos 临时连接失败:%s", formatNacosConnSummary(connectConfig))
return nil, wrapped
}
return client, nil
}
func (a *App) nacosOperationContext(config connection.ConnectionConfig) (context.Context, context.CancelFunc) {
return context.WithTimeout(
context.Background(),
time.Duration(nacosOperationTimeoutSeconds(config))*time.Second,
)
}
func nacosOperationTimeoutSeconds(config connection.ConnectionConfig) int {
if config.Timeout <= 0 {
return defaultNacosOperationTimeoutSeconds
}
return config.Timeout
}
// NacosConnect establishes and caches a Nacos connection.
func (a *App) NacosConnect(config connection.ConnectionConfig) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
_, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
logger.Error(err, "NacosConnect 连接失败:%s", formatNacosConnSummary(config))
return connection.QueryResult{Success: false, Message: err.Error()}
}
logger.Infof("NacosConnect 连接成功:%s", formatNacosConnSummary(config))
return connection.QueryResult{Success: true, Message: a.appText("nacos.backend.message.connect_success", nil)}
}
// NacosTestConnection tests connectivity without reusing long-lived cache.
func (a *App) NacosTestConnection(config connection.ConnectionConfig) connection.QueryResult {
config.Type = "nacos"
client, err := a.openNacosClientIsolated(config)
if err != nil {
logger.Error(err, "NacosTestConnection 连接失败:%s", formatNacosConnSummary(config))
return connection.QueryResult{Success: false, Message: err.Error()}
}
if client != nil {
if closeErr := client.Close(); closeErr != nil {
logger.Error(closeErr, "NacosTestConnection 释放临时连接失败:%s", formatNacosConnSummary(config))
return connection.QueryResult{
Success: false,
Message: a.appText("nacos.backend.error.test_connection_close_failed", map[string]any{"detail": closeErr.Error()}),
}
}
}
logger.Infof("NacosTestConnection 连接成功:%s", formatNacosConnSummary(config))
return connection.QueryResult{Success: true, Message: a.appText("nacos.backend.message.connect_success", nil)}
}
// NacosListNamespaces lists namespaces for a connection.
func (a *App) NacosListNamespaces(config connection.ConnectionConfig) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
namespaces, err := client.ListNamespaces(ctx)
if err != nil {
logger.Error(err, "NacosListNamespaces 失败:%s", formatNacosConnSummary(config))
result := connection.QueryResult{Success: false, Message: err.Error()}
if status, ok := nacos.HTTPStatusCode(err); ok && status == http.StatusForbidden {
result.Data = map[string]any{
"errorCode": nacosNamespaceListForbiddenErrorCode,
}
}
return result
}
return connection.QueryResult{Success: true, Data: namespaces}
}
// NacosListConfigGroups lists unique config groups under a namespace.
func (a *App) NacosListConfigGroups(config connection.ConnectionConfig, namespaceID string) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
groups, err := client.ListConfigGroups(ctx, namespaceID)
if err != nil {
logger.Error(err, "NacosListConfigGroups 失败:%s", formatNacosConnSummary(config))
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: groups}
}
// NacosSearchConfigs searches configs under a namespace.
func (a *App) NacosSearchConfigs(config connection.ConnectionConfig, query NacosConfigQuery) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
page, err := client.SearchConfigs(ctx, nacos.ConfigQuery{
NamespaceID: query.NamespaceID,
DataID: query.DataID,
Group: query.Group,
AppName: query.AppName,
PageNo: query.PageNo,
PageSize: query.PageSize,
Search: query.Search,
})
if err != nil {
logger.Error(err, "NacosSearchConfigs 失败:%s", formatNacosConnSummary(config))
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: page}
}
// NacosGetConfig loads one config.
func (a *App) NacosGetConfig(config connection.ConnectionConfig, namespaceID, group, dataID string) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
detail, err := client.GetConfig(ctx, namespaceID, group, dataID)
if err != nil {
logger.Error(err, "NacosGetConfig 失败dataId=%s group=%s", dataID, group)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: detail}
}
// NacosPublishConfig creates or updates a config.
func (a *App) NacosPublishConfig(config connection.ConnectionConfig, payload NacosPublishConfigPayload) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosDataEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.PublishConfig(ctx, nacos.PublishRequest{
NamespaceID: payload.NamespaceID,
DataID: payload.DataID,
Group: payload.Group,
Content: payload.Content,
Type: payload.Type,
AppName: payload.AppName,
Desc: payload.Desc,
BetaIPs: payload.BetaIPs,
}); err != nil {
logger.Error(err, "NacosPublishConfig 失败dataId=%s group=%s", payload.DataID, payload.Group)
return connection.QueryResult{Success: false, Message: err.Error()}
}
if strings.TrimSpace(payload.BetaIPs) != "" {
return connection.QueryResult{
Success: true,
Message: a.appText("nacos.backend.message.beta_publish_success", nil),
}
}
return connection.QueryResult{
Success: true,
Message: a.appText("nacos.backend.message.publish_success", nil),
}
}
// NacosGetBetaConfig loads beta config for one dataId/group.
func (a *App) NacosGetBetaConfig(config connection.ConnectionConfig, namespaceID, group, dataID string) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
detail, err := client.GetBetaConfig(ctx, namespaceID, group, dataID)
if err != nil {
logger.Error(err, "NacosGetBetaConfig 失败dataId=%s group=%s", dataID, group)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: detail}
}
// NacosStopBetaConfig stops beta publish.
func (a *App) NacosStopBetaConfig(config connection.ConnectionConfig, namespaceID, group, dataID string) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosDataEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.StopBetaConfig(ctx, namespaceID, group, dataID); err != nil {
logger.Error(err, "NacosStopBetaConfig 失败dataId=%s group=%s", dataID, group)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("nacos.backend.message.beta_stop_success", nil),
}
}
// NacosDeleteConfig deletes a config.
func (a *App) NacosDeleteConfig(config connection.ConnectionConfig, namespaceID, group, dataID string) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosDataEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.DeleteConfig(ctx, namespaceID, group, dataID); err != nil {
logger.Error(err, "NacosDeleteConfig 失败dataId=%s group=%s", dataID, group)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("nacos.backend.message.delete_success", nil),
}
}
// NacosCreateNamespace creates a namespace.
func (a *App) NacosCreateNamespace(config connection.ConnectionConfig, payload NacosCreateNamespacePayload) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosStructureEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.CreateNamespace(ctx, nacos.CreateNamespaceRequest{
ID: payload.ID,
ShowName: payload.ShowName,
Description: payload.Description,
}); err != nil {
logger.Error(err, "NacosCreateNamespace 失败id=%s name=%s", payload.ID, payload.ShowName)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("nacos.backend.message.namespace_create_success", nil),
}
}
// NacosUpdateNamespace updates a namespace.
func (a *App) NacosUpdateNamespace(config connection.ConnectionConfig, payload NacosUpdateNamespacePayload) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosStructureEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.UpdateNamespace(ctx, nacos.UpdateNamespaceRequest{
ID: payload.ID,
ShowName: payload.ShowName,
Description: payload.Description,
}); err != nil {
logger.Error(err, "NacosUpdateNamespace 失败id=%s", payload.ID)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("nacos.backend.message.namespace_update_success", nil),
}
}
// NacosDeleteNamespace deletes a namespace.
func (a *App) NacosDeleteNamespace(config connection.ConnectionConfig, namespaceID string) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosStructureEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.DeleteNamespace(ctx, namespaceID); err != nil {
logger.Error(err, "NacosDeleteNamespace 失败id=%s", namespaceID)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("nacos.backend.message.namespace_delete_success", nil),
}
}
// NacosListConfigHistory lists history for one config.
func (a *App) NacosListConfigHistory(config connection.ConnectionConfig, query NacosHistoryQuery) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
page, err := client.ListConfigHistory(ctx, nacos.HistoryQuery{
NamespaceID: query.NamespaceID,
DataID: query.DataID,
Group: query.Group,
PageNo: query.PageNo,
PageSize: query.PageSize,
})
if err != nil {
logger.Error(err, "NacosListConfigHistory 失败dataId=%s group=%s", query.DataID, query.Group)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: page}
}
// NacosGetConfigHistory loads one history detail.
func (a *App) NacosGetConfigHistory(config connection.ConnectionConfig, namespaceID, group, dataID, nid string) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
item, err := client.GetConfigHistory(ctx, namespaceID, group, dataID, nid)
if err != nil {
logger.Error(err, "NacosGetConfigHistory 失败nid=%s dataId=%s", nid, dataID)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: item}
}
// NacosListServices lists services under a namespace.
func (a *App) NacosListServices(config connection.ConnectionConfig, query NacosServiceQuery) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
page, err := client.ListServices(ctx, nacos.ServiceQuery{
NamespaceID: query.NamespaceID,
GroupName: query.GroupName,
PageNo: query.PageNo,
PageSize: query.PageSize,
})
if err != nil {
logger.Error(err, "NacosListServices 失败:%s", formatNacosConnSummary(config))
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: page}
}
// NacosGetService loads service detail.
func (a *App) NacosGetService(config connection.ConnectionConfig, namespaceID, serviceName, groupName string) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
detail, err := client.GetService(ctx, namespaceID, serviceName, groupName)
if err != nil {
logger.Error(err, "NacosGetService 失败service=%s", serviceName)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: detail}
}
// NacosCreateService creates a service.
func (a *App) NacosCreateService(config connection.ConnectionConfig, payload NacosServicePayload) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosStructureEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.CreateService(ctx, nacos.CreateServiceRequest{
NamespaceID: payload.NamespaceID,
ServiceName: payload.ServiceName,
GroupName: payload.GroupName,
Ephemeral: payload.Ephemeral,
ProtectThreshold: payload.ProtectThreshold,
Metadata: payload.Metadata,
}); err != nil {
logger.Error(err, "NacosCreateService 失败service=%s", payload.ServiceName)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: a.appText("nacos.backend.message.service_create_success", nil)}
}
// NacosUpdateService updates a service.
func (a *App) NacosUpdateService(config connection.ConnectionConfig, payload NacosServicePayload) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosStructureEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.UpdateService(ctx, nacos.UpdateServiceRequest{
NamespaceID: payload.NamespaceID,
ServiceName: payload.ServiceName,
GroupName: payload.GroupName,
ProtectThreshold: payload.ProtectThreshold,
Metadata: payload.Metadata,
}); err != nil {
logger.Error(err, "NacosUpdateService 失败service=%s", payload.ServiceName)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: a.appText("nacos.backend.message.service_update_success", nil)}
}
// NacosDeleteService deletes a service.
func (a *App) NacosDeleteService(config connection.ConnectionConfig, namespaceID, serviceName, groupName string) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosStructureEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.DeleteService(ctx, namespaceID, serviceName, groupName); err != nil {
logger.Error(err, "NacosDeleteService 失败service=%s", serviceName)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: a.appText("nacos.backend.message.service_delete_success", nil)}
}
// NacosListInstances lists instances of a service.
func (a *App) NacosListInstances(config connection.ConnectionConfig, query NacosInstanceQuery) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
list, err := client.ListInstances(ctx, nacos.InstanceQuery{
NamespaceID: query.NamespaceID,
ServiceName: query.ServiceName,
GroupName: query.GroupName,
Clusters: query.Clusters,
HealthyOnly: query.HealthyOnly,
})
if err != nil {
logger.Error(err, "NacosListInstances 失败service=%s", query.ServiceName)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: list}
}
// NacosGetInstance loads one instance.
func (a *App) NacosGetInstance(config connection.ConnectionConfig, payload NacosInstancePayload) connection.QueryResult {
config.Type = "nacos"
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
inst, err := client.GetInstance(ctx, toNacosInstanceRequest(payload))
if err != nil {
logger.Error(err, "NacosGetInstance 失败:%s:%d", payload.IP, payload.Port)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: inst}
}
// NacosRegisterInstance registers an instance.
func (a *App) NacosRegisterInstance(config connection.ConnectionConfig, payload NacosInstancePayload) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosDataEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.RegisterInstance(ctx, toNacosInstanceRequest(payload)); err != nil {
logger.Error(err, "NacosRegisterInstance 失败:%s:%d", payload.IP, payload.Port)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: a.appText("nacos.backend.message.instance_register_success", nil)}
}
// NacosUpdateInstance updates an instance.
func (a *App) NacosUpdateInstance(config connection.ConnectionConfig, payload NacosInstancePayload) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosDataEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.UpdateInstance(ctx, toNacosInstanceRequest(payload)); err != nil {
logger.Error(err, "NacosUpdateInstance 失败:%s:%d", payload.IP, payload.Port)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: a.appText("nacos.backend.message.instance_update_success", nil)}
}
// NacosDeregisterInstance deregisters an instance.
func (a *App) NacosDeregisterInstance(config connection.ConnectionConfig, payload NacosInstancePayload) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosDataEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.DeregisterInstance(ctx, toNacosInstanceRequest(payload)); err != nil {
logger.Error(err, "NacosDeregisterInstance 失败:%s:%d", payload.IP, payload.Port)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: a.appText("nacos.backend.message.instance_deregister_success", nil)}
}
// NacosUpdateInstanceHealth updates instance health.
func (a *App) NacosUpdateInstanceHealth(config connection.ConnectionConfig, payload NacosInstancePayload) connection.QueryResult {
config.Type = "nacos"
if err := a.ensureNacosDataEditAllowed(config); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
ctx, cancel := a.nacosOperationContext(config)
defer cancel()
client, err := a.getNacosClientWithContext(ctx, config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := client.UpdateInstanceHealth(ctx, toNacosInstanceRequest(payload)); err != nil {
logger.Error(err, "NacosUpdateInstanceHealth 失败:%s:%d", payload.IP, payload.Port)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: a.appText("nacos.backend.message.instance_health_success", nil)}
}
func toNacosInstanceRequest(payload NacosInstancePayload) nacos.InstanceRequest {
return nacos.InstanceRequest{
NamespaceID: payload.NamespaceID,
ServiceName: payload.ServiceName,
GroupName: payload.GroupName,
IP: payload.IP,
Port: payload.Port,
ClusterName: payload.ClusterName,
Weight: payload.Weight,
Enabled: payload.Enabled,
Healthy: payload.Healthy,
Ephemeral: payload.Ephemeral,
Metadata: payload.Metadata,
}
}
func (a *App) ensureNacosDataEditAllowed(config connection.ConnectionConfig) error {
// Keep the Nacos-specific message while honoring the shared production guard.
if config.ReadOnly || config.Protection.RestrictDataEdit {
return fmt.Errorf("%s", a.appText("nacos.backend.error.read_only", nil))
}
return nil
}
func (a *App) ensureNacosStructureEditAllowed(config connection.ConnectionConfig) error {
if config.ReadOnly || config.Protection.RestrictStructureEdit {
return fmt.Errorf("%s", a.appText("nacos.backend.error.read_only", nil))
}
return nil
}