diff --git a/internal/utils/apinodeutils/manager.go b/internal/utils/apinodeutils/manager.go new file mode 100644 index 00000000..5d657c98 --- /dev/null +++ b/internal/utils/apinodeutils/manager.go @@ -0,0 +1,30 @@ +// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package apinodeutils + +var SharedManager = NewManager() + +type Manager struct { + upgraderMap map[int64]*Upgrader +} + +func NewManager() *Manager { + return &Manager{ + upgraderMap: map[int64]*Upgrader{}, + } +} + +func (this *Manager) AddUpgrader(upgrader *Upgrader) { + this.upgraderMap[upgrader.apiNodeId] = upgrader +} + +func (this *Manager) FindUpgrader(apiNodeId int64) *Upgrader { + return this.upgraderMap[apiNodeId] +} + +func (this *Manager) RemoveUpgrader(upgrader *Upgrader) { + if upgrader == nil { + return + } + delete(this.upgraderMap, upgrader.apiNodeId) +} diff --git a/internal/utils/apinodeutils/upgrader.go b/internal/utils/apinodeutils/upgrader.go index ce3582e8..795a3824 100644 --- a/internal/utils/apinodeutils/upgrader.go +++ b/internal/utils/apinodeutils/upgrader.go @@ -3,7 +3,6 @@ package apinodeutils import ( - "bytes" "compress/gzip" "crypto/md5" "errors" @@ -16,9 +15,7 @@ import ( stringutil "github.com/iwind/TeaGo/utils/string" "io" "os" - "os/exec" "path/filepath" - "regexp" "runtime" ) @@ -27,49 +24,31 @@ type Progress struct { } type Upgrader struct { - progress *Progress - apiExe string + progress *Progress + apiExe string + apiNodeId int64 } -func NewUpgrader() *Upgrader { +func NewUpgrader(apiNodeId int64) *Upgrader { return &Upgrader{ - apiExe: Tea.Root + "/edge-api/bin/edge-api", - progress: &Progress{Percent: 0}, + apiExe: apiExe(), + progress: &Progress{Percent: 0}, + apiNodeId: apiNodeId, } } -func (this *Upgrader) CanUpgrade(apiVersion string) (canUpgrade bool, reason string) { - stat, err := os.Stat(this.apiExe) - if err != nil { - return false, "stat error: " + err.Error() - } - if stat.IsDir() { - return false, "is directory" - } - - localVersion, err := this.localVersion() - if err != nil { - return false, "lookup version failed: " + err.Error() - } - if stringutil.VersionCompare(localVersion, apiVersion) <= 0 { - return false, "need not upgrade, local '" + localVersion + "' vs remote '" + apiVersion + "'" - } - - return true, "" -} - -func (this *Upgrader) Upgrade(apiNodeId int64) error { +func (this *Upgrader) Upgrade() error { sharedClient, err := rpc.SharedRPC() if err != nil { return err } - apiNodeResp, err := sharedClient.APINodeRPC().FindEnabledAPINode(sharedClient.Context(0), &pb.FindEnabledAPINodeRequest{ApiNodeId: apiNodeId}) + apiNodeResp, err := sharedClient.APINodeRPC().FindEnabledAPINode(sharedClient.Context(0), &pb.FindEnabledAPINodeRequest{ApiNodeId: this.apiNodeId}) if err != nil { return err } var apiNode = apiNodeResp.ApiNode if apiNode == nil { - return errors.New("could not find api node with id '" + types.String(apiNodeId) + "'") + return errors.New("could not find api node with id '" + types.String(this.apiNodeId) + "'") } apiConfig, err := configs.LoadAPIConfig() @@ -93,12 +72,12 @@ func (this *Upgrader) Upgrade(apiNodeId int64) error { } // 检查本地文件版本 - canUpgrade, reason := this.CanUpgrade(versionResp.Version) + canUpgrade, reason := CanUpgrade(versionResp.Version, versionResp.Os, versionResp.Arch) if !canUpgrade { return errors.New(reason) } - localVersion, err := this.localVersion() + localVersion, err := localVersion() if err != nil { return errors.New("lookup version failed: " + err.Error()) } @@ -220,22 +199,3 @@ func (this *Upgrader) Upgrade(apiNodeId int64) error { func (this *Upgrader) Progress() *Progress { return this.progress } - -func (this *Upgrader) localVersion() (string, error) { - var cmd = exec.Command(this.apiExe, "-V") - var output = &bytes.Buffer{} - cmd.Stdout = output - err := cmd.Run() - if err != nil { - return "", err - } - var localVersion = output.String() - - // 检查版本号 - var reg = regexp.MustCompile(`^[\d.]+$`) - if !reg.MatchString(localVersion) { - return "", errors.New("lookup version failed: " + localVersion) - } - - return localVersion, nil -} diff --git a/internal/utils/apinodeutils/upgrader_test.go b/internal/utils/apinodeutils/upgrader_test.go index 2f92b2fe..85b055e8 100644 --- a/internal/utils/apinodeutils/upgrader_test.go +++ b/internal/utils/apinodeutils/upgrader_test.go @@ -5,17 +5,17 @@ package apinodeutils_test import ( "github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils" _ "github.com/iwind/TeaGo/bootstrap" + "runtime" "testing" ) func TestUpgrader_CanUpgrade(t *testing.T) { - var upgrader = apinodeutils.NewUpgrader() - t.Log(upgrader.CanUpgrade("0.6.3")) + t.Log(apinodeutils.CanUpgrade("0.6.3", runtime.GOOS, runtime.GOARCH)) } func TestUpgrader_Upgrade(t *testing.T) { - var upgrader = apinodeutils.NewUpgrader() - err := upgrader.Upgrade(1) + var upgrader = apinodeutils.NewUpgrader(1) + err := upgrader.Upgrade() if err != nil { t.Fatal(err) } diff --git a/internal/utils/apinodeutils/utils.go b/internal/utils/apinodeutils/utils.go new file mode 100644 index 00000000..2d34f637 --- /dev/null +++ b/internal/utils/apinodeutils/utils.go @@ -0,0 +1,76 @@ +// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package apinodeutils + +import ( + "bytes" + "errors" + teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" + "github.com/iwind/TeaGo/Tea" + stringutil "github.com/iwind/TeaGo/utils/string" + "os" + "os/exec" + "regexp" + "runtime" + "strings" +) + +func CanUpgrade(apiVersion string, osName string, arch string) (canUpgrade bool, reason string) { + if len(apiVersion) == 0 { + return false, "current api version should not be empty" + } + + if osName != runtime.GOOS { + return false, "os not match: " + osName + } + if arch != runtime.GOARCH { + return false, "arch not match: " + arch + } + + stat, err := os.Stat(apiExe()) + if err != nil { + return false, "stat error: " + err.Error() + } + if stat.IsDir() { + return false, "is directory" + } + + localVersion, err := localVersion() + if err != nil { + return false, "lookup version failed: " + err.Error() + } + if localVersion != teaconst.APINodeVersion { + return false, "not newest api node" + } + if stringutil.VersionCompare(localVersion, apiVersion) <= 0 { + return false, "need not upgrade, local '" + localVersion + "' vs remote '" + apiVersion + "'" + } + + return true, "" +} + + + +func localVersion() (string, error) { + var cmd = exec.Command(apiExe(), "-V") + var output = &bytes.Buffer{} + cmd.Stdout = output + err := cmd.Run() + if err != nil { + return "", err + } + var localVersion = strings.TrimSpace(output.String()) + + // 检查版本号 + var reg = regexp.MustCompile(`^[\d.]+$`) + if !reg.MatchString(localVersion) { + return "", errors.New("lookup version failed: " + localVersion) + } + + return localVersion, nil +} + + +func apiExe() string { + return Tea.Root + "/edge-api/bin/edge-api" +} \ No newline at end of file diff --git a/internal/web/actions/default/api/index.go b/internal/web/actions/default/api/index.go index afa02adf..815bea00 100644 --- a/internal/web/actions/default/api/index.go +++ b/internal/web/actions/default/api/index.go @@ -3,12 +3,15 @@ package api import ( "encoding/json" "fmt" + teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" + "github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/iwind/TeaGo/logs" "github.com/iwind/TeaGo/maps" + stringutil "github.com/iwind/TeaGo/utils/string" timeutil "github.com/iwind/TeaGo/utils/time" "time" ) @@ -44,7 +47,7 @@ func (this *IndexAction) RunGet(params struct{}) { for _, node := range nodesResp.ApiNodes { // 状态 - status := &nodeconfigs.NodeStatus{} + var status = &nodeconfigs.NodeStatus{} if len(node.StatusJSON) > 0 { err = json.Unmarshal(node.StatusJSON, &status) if err != nil { @@ -55,7 +58,7 @@ func (this *IndexAction) RunGet(params struct{}) { } // Rest地址 - restAccessAddrs := []string{} + var restAccessAddrs = []string{} if node.RestIsOn { if len(node.RestHTTPJSON) > 0 { httpConfig := &serverconfigs.HTTPProtocolConfig{} @@ -86,6 +89,9 @@ func (this *IndexAction) RunGet(params struct{}) { } } + var shouldUpgrade = status.IsActive && len(status.BuildVersion) > 0 && stringutil.VersionCompare(teaconst.APINodeVersion, status.BuildVersion) > 0 + canUpgrade, _ := apinodeutils.CanUpgrade(status.BuildVersion, status.OS, status.Arch) + nodeMaps = append(nodeMaps, maps.Map{ "id": node.Id, "isOn": node.IsOn, @@ -94,14 +100,17 @@ func (this *IndexAction) RunGet(params struct{}) { "restAccessAddrs": restAccessAddrs, "isPrimary": node.IsPrimary, "status": maps.Map{ - "isActive": status.IsActive, - "updatedAt": status.UpdatedAt, - "hostname": status.Hostname, - "cpuUsage": status.CPUUsage, - "cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100), - "memUsage": status.MemoryUsage, - "memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100), - "buildVersion": status.BuildVersion, + "isActive": status.IsActive, + "updatedAt": status.UpdatedAt, + "hostname": status.Hostname, + "cpuUsage": status.CPUUsage, + "cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100), + "memUsage": status.MemoryUsage, + "memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100), + "buildVersion": status.BuildVersion, + "latestVersion": teaconst.APINodeVersion, + "shouldUpgrade": shouldUpgrade, + "canUpgrade": shouldUpgrade && canUpgrade, }, }) } diff --git a/internal/web/actions/default/api/node/init.go b/internal/web/actions/default/api/node/init.go index 96bdb04a..66c2aee5 100644 --- a/internal/web/actions/default/api/node/init.go +++ b/internal/web/actions/default/api/node/init.go @@ -24,7 +24,10 @@ func init() { GetPost("/update", new(UpdateAction)). Get("/install", new(InstallAction)). Get("/logs", new(LogsAction)). + GetPost("/upgradePopup", new(UpgradePopupAction)). + Post("/upgradeCheck", new(UpgradeCheckAction)). + // EndAll() }) } diff --git a/internal/web/actions/default/api/node/upgradeCheck.go b/internal/web/actions/default/api/node/upgradeCheck.go new file mode 100644 index 00000000..0b06b1b0 --- /dev/null +++ b/internal/web/actions/default/api/node/upgradeCheck.go @@ -0,0 +1,67 @@ +// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package node + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configs" + teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" + "github.com/TeaOSLab/EdgeAdmin/internal/rpc" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" +) + +// UpgradeCheckAction 检查升级结果 +type UpgradeCheckAction struct { + actionutils.ParentAction +} + +func (this *UpgradeCheckAction) Init() { + this.Nav("", "", "") +} + +func (this *UpgradeCheckAction) RunPost(params struct { + NodeId int64 +}) { + this.Data["isOk"] = false + + nodeResp, err := this.RPC().APINodeRPC().FindEnabledAPINode(this.AdminContext(), &pb.FindEnabledAPINodeRequest{ApiNodeId: params.NodeId}) + if err != nil { + this.Success() + return + } + + var node = nodeResp.ApiNode + if node == nil || len(node.AccessAddrs) == 0 { + this.Success() + return + } + + apiConfig, err := configs.LoadAPIConfig() + if err != nil { + this.Success() + return + } + + var newAPIConfig = apiConfig.Clone() + newAPIConfig.RPC.Endpoints = node.AccessAddrs + rpcClient, err := rpc.NewRPCClient(newAPIConfig, false) + if err != nil { + this.Success() + return + } + + versionResp, err := rpcClient.APINodeRPC().FindCurrentAPINodeVersion(rpcClient.Context(0), &pb.FindCurrentAPINodeVersionRequest{}) + if err != nil { + this.Success() + return + } + + if versionResp.Version != teaconst.Version { + this.Success() + return + } + + this.Data["isOk"] = true + + this.Success() +} diff --git a/internal/web/actions/default/api/node/upgradePopup.go b/internal/web/actions/default/api/node/upgradePopup.go new file mode 100644 index 00000000..edf0dc55 --- /dev/null +++ b/internal/web/actions/default/api/node/upgradePopup.go @@ -0,0 +1,124 @@ +// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package node + +import ( + "encoding/json" + "errors" + teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" + "github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/actions" + "strings" +) + +type UpgradePopupAction struct { + actionutils.ParentAction +} + +func (this *UpgradePopupAction) Init() { + this.Nav("", "", "") +} + +func (this *UpgradePopupAction) RunGet(params struct { + NodeId int64 +}) { + this.Data["nodeId"] = params.NodeId + this.Data["nodeName"] = "" + this.Data["currentVersion"] = "" + this.Data["latestVersion"] = "" + this.Data["result"] = "" + this.Data["resultIsOk"] = true + this.Data["canUpgrade"] = false + this.Data["isUpgrading"] = false + + nodeResp, err := this.RPC().APINodeRPC().FindEnabledAPINode(this.AdminContext(), &pb.FindEnabledAPINodeRequest{ApiNodeId: params.NodeId}) + if err != nil { + this.ErrorPage(err) + return + } + var node = nodeResp.ApiNode + if node == nil { + this.Data["result"] = "要升级的节点不存在" + this.Data["resultIsOk"] = false + this.Show() + return + } + this.Data["nodeName"] = node.Name + " / [" + strings.Join(node.AccessAddrs, ", ") + "]" + + // 节点状态 + var status = &nodeconfigs.NodeStatus{} + if len(node.StatusJSON) > 0 { + err = json.Unmarshal(node.StatusJSON, &status) + if err != nil { + this.ErrorPage(errors.New("decode status failed: " + err.Error())) + return + } + this.Data["currentVersion"] = status.BuildVersion + } else { + this.Data["result"] = "无法检测到节点当前版本" + this.Data["resultIsOk"] = false + this.Show() + return + } + this.Data["latestVersion"] = teaconst.APINodeVersion + + if status.IsActive && len(status.BuildVersion) > 0 { + canUpgrade, reason := apinodeutils.CanUpgrade(status.BuildVersion, status.OS, status.Arch) + if !canUpgrade { + this.Data["result"] = reason + this.Data["resultIsOk"] = false + this.Show() + return + } + this.Data["canUpgrade"] = true + this.Data["result"] = "等待升级" + this.Data["resultIsOk"] = true + } else { + this.Data["result"] = "当前节点非连接状态无法远程升级" + this.Data["resultIsOk"] = false + this.Show() + return + } + + // 是否正在升级 + var oldUpgrader = apinodeutils.SharedManager.FindUpgrader(params.NodeId) + if oldUpgrader != nil { + this.Data["result"] = "正在升级中..." + this.Data["resultIsOk"] = false + this.Data["isUpgrading"] = true + } + + this.Show() +} + +func (this *UpgradePopupAction) RunPost(params struct { + NodeId int64 + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + var manager = apinodeutils.SharedManager + + var oldUpgrader = manager.FindUpgrader(params.NodeId) + if oldUpgrader != nil { + this.Fail("正在升级中,无需重复提交 ...") + return + } + + var upgrader = apinodeutils.NewUpgrader(params.NodeId) + manager.AddUpgrader(upgrader) + defer func() { + manager.RemoveUpgrader(upgrader) + }() + + err := upgrader.Upgrade() + if err != nil { + this.Fail("升级失败:" + err.Error()) + return + } + + this.Success() +} diff --git a/web/views/@default/api/index.html b/web/views/@default/api/index.html index 83ddb1e2..335954fa 100644 --- a/web/views/@default/api/index.html +++ b/web/views/@default/api/index.html @@ -23,7 +23,12 @@ {{node.name}} - 主节点 +
+ 主节点 +
+
+ v{{node.status.buildVersion}} -> v{{node.status.latestVersion}}
[远程升级]
+
diff --git a/web/views/@default/api/index.js b/web/views/@default/api/index.js index 02b7df8e..c7606eff 100644 --- a/web/views/@default/api/index.js +++ b/web/views/@default/api/index.js @@ -1,7 +1,7 @@ Tea.context(function () { // 创建节点 this.createNode = function () { - teaweb.popup("/api/node/createPopup", { + teaweb.popup(".node.createPopup", { width: "50em", height: "30em", callback: function () { @@ -16,11 +16,20 @@ Tea.context(function () { this.deleteNode = function (nodeId) { let that = this teaweb.confirm("确定要删除此节点吗?", function () { - that.$post("/api/delete") + that.$post(".delete") .params({ nodeId: nodeId }) .refresh() }) } + + // 升级节点 + this.upgradeNode = function (nodeId) { + teaweb.popup(".node.upgradePopup?nodeId=" + nodeId, { + onClose: function () { + teaweb.reload() + } + }) + } }) \ No newline at end of file diff --git a/web/views/@default/api/node/upgradePopup.html b/web/views/@default/api/node/upgradePopup.html new file mode 100644 index 00000000..5ce6b25d --- /dev/null +++ b/web/views/@default/api/node/upgradePopup.html @@ -0,0 +1,35 @@ +{$layout "layout_popup"} + +

远程升级

+ +
+ + + + + + + + + + + + + + + + + + + + +
API节点{{nodeName}}
当前版本v{{currentVersion}}
目标版本v{{latestVersion}}
升级结果 + 已经升级到最新版本 + + {{result}} + 升级中... + +
+ + 开始升级 +
\ No newline at end of file diff --git a/web/views/@default/api/node/upgradePopup.js b/web/views/@default/api/node/upgradePopup.js new file mode 100644 index 00000000..d0796fe2 --- /dev/null +++ b/web/views/@default/api/node/upgradePopup.js @@ -0,0 +1,39 @@ +Tea.context(function () { + this.$delay(function () { + this.checkLoop() + }) + + this.success = function () { + } + + this.isRequesting = false + + this.before = function () { + this.isRequesting = true + } + + this.done = function () { + this.isRequesting = false + } + + this.checkLoop = function () { + if (this.currentVersion == this.latestVersion) { + return + } + + this.$post(".upgradeCheck") + .params({ + nodeId: this.nodeId + }) + .success(function (resp) { + if (resp.data.isOk) { + teaweb.reload() + } + }) + .done(function () { + this.$delay(function () { + this.checkLoop() + }, 3000) + }) + } +}) \ No newline at end of file