package console import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" "yunyan/comm" ) // console → 业务部署的「热重载」调用。 // // console 是单例服务(lego/base/single)、不接 ETCD/rpcx 集群,无法直接 RPC 业务服务, // 只能走该部署的公网网关入口(app_registry.base_url)。鉴权用 ${FIELD_ENCRYPT_KEY} 的 HMAC 签名, // 由网关校验(见 comm/consolesign.go)——复用这把两边本就必须一致的密钥,不再多引入一把要同步的。 const ( reloadRoute = "api_reloadmoduleconfig" resetRoute = "api_resetmoduleconfig" reloadTimeout = 5 * time.Second ) var reloadClient = &http.Client{Timeout: reloadTimeout} // callModuleConfigReload 通知目标部署重读业务库并热替换客户端。 func callModuleConfigReload(baseUrl, encKey string) (*comm.ConfigApplyReport, error) { return callConsoleRoute(baseUrl, encKey, reloadRoute, "") } // callModuleConfigReset 让目标部署把某模块的配置还原为它自己 confs/*.yaml 的初始值。 // yaml 原值只存在于业务服务内部,故必须由它自己完成——明文密钥一步都不离开该服务。 func callModuleConfigReset(baseUrl, encKey, module string) (*comm.ConfigApplyReport, error) { return callConsoleRoute(baseUrl, encKey, resetRoute, module) } // callConsoleRoute 调目标部署的 console 专用运维接口并返回它的回执。 // base_url 未填 / 网络不通 / 服务未起 / 验签失败 → 返回 error;调用方须如实呈现失败, // 绝不能默认当成功。arg 会并入签名,网关校验后经 Meta 传给业务侧。 func callConsoleRoute(baseUrl, encKey, route, arg string) (*comm.ConfigApplyReport, error) { base := strings.TrimRight(strings.TrimSpace(baseUrl), "/") if base == "" { return nil, fmt.Errorf("该部署未填写对外地址(base_url),无法通知它") } if !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") { base = "https://" + base } ts := strconv.FormatInt(time.Now().Unix(), 10) req, err := http.NewRequest(http.MethodPost, base+"/api/api/"+route, bytes.NewReader([]byte("{}"))) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set(comm.ConsoleTsHeader, ts) req.Header.Set(comm.ConsoleArgHeader, arg) req.Header.Set(comm.ConsoleSignHeader, comm.SignConsoleCall(encKey, ts, route, arg)) resp, err := reloadClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("重载接口 HTTP %d: %s", resp.StatusCode, truncate(string(body), 200)) } // 成功时 api 服务返回 ConfigApplyReport 的原样 JSON;出错时网关/业务返回 HttpResult{code,msg}。 // 用 Kind 是否落位来区分两者——HttpResult 里没有这个字段。 var report comm.ConfigApplyReport if e := json.Unmarshal(body, &report); e == nil && report.Kind != "" { return &report, nil } var hr struct { Code int `json:"code"` Message string `json:"msg"` } if json.Unmarshal(body, &hr) == nil && hr.Message != "" { return nil, fmt.Errorf("重载被拒绝(code=%d): %s", hr.Code, hr.Message) } return nil, fmt.Errorf("重载接口返回无法识别的响应: %s", truncate(string(body), 200)) } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] + "…" }