|
@@ -84,6 +84,14 @@ func (l *UpdateServiceLogic) UpdateService(req *types.UpdateServiceReq) (*types.
|
|
|
mountWarning, mountStatus = l.applyMountChanges(svc, oldDirs, newDirs, req)
|
|
mountWarning, mountStatus = l.applyMountChanges(svc, oldDirs, newDirs, req)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 编辑联动失败时,服务状态与挂载结果强绑定(与新增服务同规则):
|
|
|
|
|
+ // 挂载/卸载任一失败 → mount_status=failed 且 status 同步置"停用",
|
|
|
|
|
+ // 避免"启用+failed"的错位组合;req.Directories==nil 的调用方(状态开关)不受影响。
|
|
|
|
|
+ finalStatus := req.Status
|
|
|
|
|
+ if req.Directories != nil && mountStatus == "failed" {
|
|
|
|
|
+ finalStatus = "停用"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 事务:更新服务 + 目录全删全插(与新增服务同款后端收口模式,任一步失败整体回滚)
|
|
// 事务:更新服务 + 目录全删全插(与新增服务同款后端收口模式,任一步失败整体回滚)
|
|
|
err = l.svcCtx.DB.Transact(func(session sqlx.Session) error {
|
|
err = l.svcCtx.DB.Transact(func(session sqlx.Session) error {
|
|
|
svcModel := l.svcCtx.ServiceModel.WithSession(session)
|
|
svcModel := l.svcCtx.ServiceModel.WithSession(session)
|
|
@@ -97,7 +105,7 @@ func (l *UpdateServiceLogic) UpdateService(req *types.UpdateServiceReq) (*types.
|
|
|
Iden: nullString(req.Iden),
|
|
Iden: nullString(req.Iden),
|
|
|
KeyIden: nullString(req.KeyIden),
|
|
KeyIden: nullString(req.KeyIden),
|
|
|
KeyAlgo: nullString(req.KeyAlgo),
|
|
KeyAlgo: nullString(req.KeyAlgo),
|
|
|
- Status: nullString(req.Status),
|
|
|
|
|
|
|
+ Status: nullString(finalStatus),
|
|
|
MountStatus: nullString(mountStatus), // 联动算出的新状态(没联动就是旧值)
|
|
MountStatus: nullString(mountStatus), // 联动算出的新状态(没联动就是旧值)
|
|
|
Brush: req.Brush, // 一键刷库配置:编辑只落库改配置,不追溯触发刷库(刷库编排仅在创建流程)
|
|
Brush: req.Brush, // 一键刷库配置:编辑只落库改配置,不追溯触发刷库(刷库编排仅在创建流程)
|
|
|
}
|
|
}
|
|
@@ -157,23 +165,31 @@ func (l *UpdateServiceLogic) applyMountChanges(svc *model.TService, oldDirs, new
|
|
|
toUnmount = diffStrings(oldDirs, newDirs)
|
|
toUnmount = diffStrings(oldDirs, newDirs)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 已挂载(mounted)的服务编辑时不重新触发挂载:剩余目录仍挂在外挂上,
|
|
|
|
|
+ // 重发挂载请求只是白跑一趟幂等检查;若外挂恰好不可达或鉴权不匹配,
|
|
|
|
|
+ // 反而会把好端端的 mounted 服务打成 failed。需要补挂/换机重挂时,
|
|
|
|
|
+ // 走列表开关"停用(卸载)→启用(挂载)"的正规路径。
|
|
|
|
|
+ needMount := svc.MountStatus.String != "mounted"
|
|
|
|
|
+
|
|
|
var msgs []string
|
|
var msgs []string
|
|
|
- // 1. 先卸载:发往【旧】IP(挂载记录登记在旧机器的外挂程序进程里)
|
|
|
|
|
|
|
+ // 1. 先卸载(清理动作,已挂载服务也保留):发往【旧】IP(挂载记录登记在旧机器的外挂程序进程里)
|
|
|
if len(toUnmount) > 0 {
|
|
if len(toUnmount) > 0 {
|
|
|
oldIp, oldPort := svc.Ip.String, svc.Port // port 已是 int64
|
|
oldIp, oldPort := svc.Ip.String, svc.Port // port 已是 int64
|
|
|
if oldIp == "" || oldPort == 0 {
|
|
if oldIp == "" || oldPort == 0 {
|
|
|
msgs = append(msgs, "原外挂 IP/端口为空,无法联动卸载,旧目录可能仍处于挂载状态")
|
|
msgs = append(msgs, "原外挂 IP/端口为空,无法联动卸载,旧目录可能仍处于挂载状态")
|
|
|
} else {
|
|
} else {
|
|
|
for _, dir := range toUnmount {
|
|
for _, dir := range toUnmount {
|
|
|
- if w := l.fuseUnmountOne(oldIp, oldPort, dir); w != "" {
|
|
|
|
|
|
|
+ // 卸载发往旧机器,用旧 iden 作 X-TOKEN(旧机器的外挂 TOKEN 按旧值配置)
|
|
|
|
|
+ if w := l.fuseUnmountOne(svc.Iden.String, oldIp, oldPort, dir); w != "" {
|
|
|
msgs = append(msgs, w)
|
|
msgs = append(msgs, w)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 再挂载:发往【新】IP,新目录全量下发(幂等表自动跳过已挂的)
|
|
|
|
|
- if len(newDirs) > 0 {
|
|
|
|
|
|
|
+ // 2. 再挂载:仅未挂载(unmounted/failed)的服务才全量下发,
|
|
|
|
|
+ // 幂等表自动跳过已挂的目录,顺带把 failed 服务漏挂的目录补挂上(自愈)。
|
|
|
|
|
+ if needMount && len(newDirs) > 0 {
|
|
|
if req.Ip == "" || req.Port == 0 {
|
|
if req.Ip == "" || req.Port == 0 {
|
|
|
msgs = append(msgs, "新外挂 IP/端口为空,无法联动挂载")
|
|
msgs = append(msgs, "新外挂 IP/端口为空,无法联动挂载")
|
|
|
} else {
|
|
} else {
|
|
@@ -182,7 +198,8 @@ func (l *UpdateServiceLogic) applyMountChanges(svc *model.TService, oldDirs, new
|
|
|
msgs = append(msgs, fmt.Sprintf("联动挂载失败: %v", err))
|
|
msgs = append(msgs, fmt.Sprintf("联动挂载失败: %v", err))
|
|
|
} else {
|
|
} else {
|
|
|
for _, dir := range newDirs {
|
|
for _, dir := range newDirs {
|
|
|
- if w := l.fuseMountOne(req.Ip, req.Port, dir, ikey); w != "" {
|
|
|
|
|
|
|
+ // 挂载发往新机器,用新 iden 作 X-TOKEN(新机器的外挂 TOKEN 按新值配置)
|
|
|
|
|
+ if w := l.fuseMountOne(req.Iden, req.Ip, req.Port, dir, ikey); w != "" {
|
|
|
msgs = append(msgs, w)
|
|
msgs = append(msgs, w)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -191,25 +208,27 @@ func (l *UpdateServiceLogic) applyMountChanges(svc *model.TService, oldDirs, new
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 3. mount_status 重算:
|
|
// 3. mount_status 重算:
|
|
|
- // 无动作(没卸也没挂,比如目录原样重存) → 保持原值;
|
|
|
|
|
- // 有动作全成功 → 还有新目录 = mounted,清空了目录 = unmounted;
|
|
|
|
|
- // 有动作任一失败 → failed(同时返回聚合警告,前端弹黄条)。
|
|
|
|
|
- hasAction := len(toUnmount) > 0 || len(newDirs) > 0
|
|
|
|
|
- if !hasAction {
|
|
|
|
|
- return "", svc.MountStatus.String
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 任一动作失败 → failed(同时返回聚合警告,主流程会把 status 一并置"停用");
|
|
|
|
|
+ // 未挂载服务走了挂载 → 全成功有目录 = mounted,清空目录 = unmounted;
|
|
|
|
|
+ // 已挂载服务(未重新挂载)→ 换机全卸了 = unmounted,否则剩余目录仍挂着 = 保持 mounted。
|
|
|
if len(msgs) > 0 {
|
|
if len(msgs) > 0 {
|
|
|
return "编辑已保存,但挂载联动存在失败: " + strings.Join(msgs, "; "), "failed"
|
|
return "编辑已保存,但挂载联动存在失败: " + strings.Join(msgs, "; "), "failed"
|
|
|
}
|
|
}
|
|
|
- if len(newDirs) > 0 {
|
|
|
|
|
- return "", "mounted"
|
|
|
|
|
|
|
+ if needMount {
|
|
|
|
|
+ if len(newDirs) > 0 {
|
|
|
|
|
+ return "", "mounted"
|
|
|
|
|
+ }
|
|
|
|
|
+ return "", "unmounted"
|
|
|
|
|
+ }
|
|
|
|
|
+ if ipChanged && len(toUnmount) > 0 {
|
|
|
|
|
+ return "", "unmounted" // 换机把旧挂载全卸了,新机器上没有挂载
|
|
|
}
|
|
}
|
|
|
- return "", "unmounted"
|
|
|
|
|
|
|
+ return "", svc.MountStatus.String
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// fuseMountOne 通知外挂程序挂载单个目录。成功返回空串,失败返回警告文案。
|
|
// fuseMountOne 通知外挂程序挂载单个目录。成功返回空串,失败返回警告文案。
|
|
|
// 请求体与新增服务完全同款:{backing: 目录+"-cipher", mount: 目录, ikey: 密钥内容}
|
|
// 请求体与新增服务完全同款:{backing: 目录+"-cipher", mount: 目录, ikey: 密钥内容}
|
|
|
-func (l *UpdateServiceLogic) fuseMountOne(ip string, port int64, mount, ikey string) string {
|
|
|
|
|
|
|
+func (l *UpdateServiceLogic) fuseMountOne(iden, ip string, port int64, mount, ikey string) string {
|
|
|
// path.Clean(非 filepath!):路径要发给远程 Linux 外挂程序,必须保持 / 分隔符
|
|
// path.Clean(非 filepath!):路径要发给远程 Linux 外挂程序,必须保持 / 分隔符
|
|
|
dirLinuxPath := path.Clean(mount)
|
|
dirLinuxPath := path.Clean(mount)
|
|
|
payload := map[string]interface{}{
|
|
payload := map[string]interface{}{
|
|
@@ -225,7 +244,8 @@ func (l *UpdateServiceLogic) fuseMountOne(ip string, port int64, mount, ikey str
|
|
|
// DoSimple 记录 type=2 接口日志,父 Span 指向本次入站请求
|
|
// DoSimple 记录 type=2 接口日志,父 Span 指向本次入站请求
|
|
|
if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseMount", baseURL, body,
|
|
if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseMount", baseURL, body,
|
|
|
func() (map[string]interface{}, int64, error) {
|
|
func() (map[string]interface{}, int64, error) {
|
|
|
- resp, rerr := fuseMountClient.Post(baseURL, "application/json", bytes.NewReader(body))
|
|
|
|
|
|
|
+ // doFuseRequest 注入 X-TOKEN=iden(接入标识);client.Post 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
|
|
|
|
|
+ resp, rerr := doFuseRequest(fuseMountClient, iden, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
|
|
|
if rerr != nil {
|
|
if rerr != nil {
|
|
|
return nil, 0, fmt.Errorf("挂载目录 %s 失败(外挂程序未启动?): %w", dirLinuxPath, rerr)
|
|
return nil, 0, fmt.Errorf("挂载目录 %s 失败(外挂程序未启动?): %w", dirLinuxPath, rerr)
|
|
|
}
|
|
}
|
|
@@ -245,7 +265,7 @@ func (l *UpdateServiceLogic) fuseMountOne(ip string, port int64, mount, ikey str
|
|
|
|
|
|
|
|
// fuseUnmountOne 通知外挂程序卸载单个挂载点。成功返回空串,失败返回警告文案。
|
|
// fuseUnmountOne 通知外挂程序卸载单个挂载点。成功返回空串,失败返回警告文案。
|
|
|
// 卸载接口幂等:挂载点不在外挂注册表中也返回成功,所以重存/多卸都零成本。
|
|
// 卸载接口幂等:挂载点不在外挂注册表中也返回成功,所以重存/多卸都零成本。
|
|
|
-func (l *UpdateServiceLogic) fuseUnmountOne(ip string, port int64, mount string) string {
|
|
|
|
|
|
|
+func (l *UpdateServiceLogic) fuseUnmountOne(iden, ip string, port int64, mount string) string {
|
|
|
cleanMount := path.Clean(mount) // 同上,发给 Linux 必须保持 / 分隔符
|
|
cleanMount := path.Clean(mount) // 同上,发给 Linux 必须保持 / 分隔符
|
|
|
baseURL := fmt.Sprintf("http://%s:%d/api/fuseUnMount", ip, port) // %d:port 是 int64
|
|
baseURL := fmt.Sprintf("http://%s:%d/api/fuseUnMount", ip, port) // %d:port 是 int64
|
|
|
reqURL, uerr := url.Parse(baseURL)
|
|
reqURL, uerr := url.Parse(baseURL)
|
|
@@ -259,7 +279,8 @@ func (l *UpdateServiceLogic) fuseUnmountOne(ip string, port int64, mount string)
|
|
|
if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", finalURL,
|
|
if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", finalURL,
|
|
|
audit.BuildRemoteGetRequest(http.MethodGet, finalURL),
|
|
audit.BuildRemoteGetRequest(http.MethodGet, finalURL),
|
|
|
func() (map[string]interface{}, int64, error) {
|
|
func() (map[string]interface{}, int64, error) {
|
|
|
- resp, rerr := fuseMountClient.Get(finalURL)
|
|
|
|
|
|
|
+ // doFuseRequest 注入 X-TOKEN=iden(接入标识);client.Get 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
|
|
|
|
|
+ resp, rerr := doFuseRequest(fuseMountClient, iden, http.MethodGet, finalURL, nil, "")
|
|
|
if rerr != nil {
|
|
if rerr != nil {
|
|
|
return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
|
|
return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
|
|
|
}
|
|
}
|