实现API节点远程升级

This commit is contained in:
GoEdgeLab
2023-03-05 12:05:18 +08:00
parent b5cba51456
commit 2ac7f6d14a
12 changed files with 426 additions and 69 deletions

View File

@@ -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)
}

View File

@@ -3,7 +3,6 @@
package apinodeutils package apinodeutils
import ( import (
"bytes"
"compress/gzip" "compress/gzip"
"crypto/md5" "crypto/md5"
"errors" "errors"
@@ -16,9 +15,7 @@ import (
stringutil "github.com/iwind/TeaGo/utils/string" stringutil "github.com/iwind/TeaGo/utils/string"
"io" "io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp"
"runtime" "runtime"
) )
@@ -27,49 +24,31 @@ type Progress struct {
} }
type Upgrader struct { type Upgrader struct {
progress *Progress progress *Progress
apiExe string apiExe string
apiNodeId int64
} }
func NewUpgrader() *Upgrader { func NewUpgrader(apiNodeId int64) *Upgrader {
return &Upgrader{ return &Upgrader{
apiExe: Tea.Root + "/edge-api/bin/edge-api", apiExe: apiExe(),
progress: &Progress{Percent: 0}, progress: &Progress{Percent: 0},
apiNodeId: apiNodeId,
} }
} }
func (this *Upgrader) CanUpgrade(apiVersion string) (canUpgrade bool, reason string) { func (this *Upgrader) Upgrade() error {
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 {
sharedClient, err := rpc.SharedRPC() sharedClient, err := rpc.SharedRPC()
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
var apiNode = apiNodeResp.ApiNode var apiNode = apiNodeResp.ApiNode
if apiNode == nil { 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() 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 { if !canUpgrade {
return errors.New(reason) return errors.New(reason)
} }
localVersion, err := this.localVersion() localVersion, err := localVersion()
if err != nil { if err != nil {
return errors.New("lookup version failed: " + err.Error()) return errors.New("lookup version failed: " + err.Error())
} }
@@ -220,22 +199,3 @@ func (this *Upgrader) Upgrade(apiNodeId int64) error {
func (this *Upgrader) Progress() *Progress { func (this *Upgrader) Progress() *Progress {
return this.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
}

View File

@@ -5,17 +5,17 @@ package apinodeutils_test
import ( import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils" "github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils"
_ "github.com/iwind/TeaGo/bootstrap" _ "github.com/iwind/TeaGo/bootstrap"
"runtime"
"testing" "testing"
) )
func TestUpgrader_CanUpgrade(t *testing.T) { func TestUpgrader_CanUpgrade(t *testing.T) {
var upgrader = apinodeutils.NewUpgrader() t.Log(apinodeutils.CanUpgrade("0.6.3", runtime.GOOS, runtime.GOARCH))
t.Log(upgrader.CanUpgrade("0.6.3"))
} }
func TestUpgrader_Upgrade(t *testing.T) { func TestUpgrader_Upgrade(t *testing.T) {
var upgrader = apinodeutils.NewUpgrader() var upgrader = apinodeutils.NewUpgrader(1)
err := upgrader.Upgrade(1) err := upgrader.Upgrade()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -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"
}

View File

@@ -3,12 +3,15 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "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/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/logs" "github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
stringutil "github.com/iwind/TeaGo/utils/string"
timeutil "github.com/iwind/TeaGo/utils/time" timeutil "github.com/iwind/TeaGo/utils/time"
"time" "time"
) )
@@ -44,7 +47,7 @@ func (this *IndexAction) RunGet(params struct{}) {
for _, node := range nodesResp.ApiNodes { for _, node := range nodesResp.ApiNodes {
// 状态 // 状态
status := &nodeconfigs.NodeStatus{} var status = &nodeconfigs.NodeStatus{}
if len(node.StatusJSON) > 0 { if len(node.StatusJSON) > 0 {
err = json.Unmarshal(node.StatusJSON, &status) err = json.Unmarshal(node.StatusJSON, &status)
if err != nil { if err != nil {
@@ -55,7 +58,7 @@ func (this *IndexAction) RunGet(params struct{}) {
} }
// Rest地址 // Rest地址
restAccessAddrs := []string{} var restAccessAddrs = []string{}
if node.RestIsOn { if node.RestIsOn {
if len(node.RestHTTPJSON) > 0 { if len(node.RestHTTPJSON) > 0 {
httpConfig := &serverconfigs.HTTPProtocolConfig{} 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{ nodeMaps = append(nodeMaps, maps.Map{
"id": node.Id, "id": node.Id,
"isOn": node.IsOn, "isOn": node.IsOn,
@@ -94,14 +100,17 @@ func (this *IndexAction) RunGet(params struct{}) {
"restAccessAddrs": restAccessAddrs, "restAccessAddrs": restAccessAddrs,
"isPrimary": node.IsPrimary, "isPrimary": node.IsPrimary,
"status": maps.Map{ "status": maps.Map{
"isActive": status.IsActive, "isActive": status.IsActive,
"updatedAt": status.UpdatedAt, "updatedAt": status.UpdatedAt,
"hostname": status.Hostname, "hostname": status.Hostname,
"cpuUsage": status.CPUUsage, "cpuUsage": status.CPUUsage,
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100), "cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
"memUsage": status.MemoryUsage, "memUsage": status.MemoryUsage,
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100), "memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
"buildVersion": status.BuildVersion, "buildVersion": status.BuildVersion,
"latestVersion": teaconst.APINodeVersion,
"shouldUpgrade": shouldUpgrade,
"canUpgrade": shouldUpgrade && canUpgrade,
}, },
}) })
} }

View File

@@ -24,7 +24,10 @@ func init() {
GetPost("/update", new(UpdateAction)). GetPost("/update", new(UpdateAction)).
Get("/install", new(InstallAction)). Get("/install", new(InstallAction)).
Get("/logs", new(LogsAction)). Get("/logs", new(LogsAction)).
GetPost("/upgradePopup", new(UpgradePopupAction)).
Post("/upgradeCheck", new(UpgradeCheckAction)).
//
EndAll() EndAll()
}) })
} }

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -23,7 +23,12 @@
</thead> </thead>
<tr v-for="node in nodes"> <tr v-for="node in nodes">
<td><a :href="'/api/node?nodeId=' + node.id">{{node.name}}</a> <td><a :href="'/api/node?nodeId=' + node.id">{{node.name}}</a>
<grey-label v-if="node.isPrimary">主节点</grey-label> <div v-if="node.isPrimary">
<grey-label v-if="node.isPrimary">主节点</grey-label>
</div>
<div v-if="node.status != null && node.status.shouldUpgrade">
<span class="red small">v{{node.status.buildVersion}} -&gt; v{{node.status.latestVersion}}<br/><a href="" v-if="node.status.canUpgrade" @click.prevent="upgradeNode(node.id)">[远程升级]</a> </span>
</div>
</td> </td>
<td> <td>
<div v-if="node.accessAddrs != null && node.accessAddrs.length > 0"> <div v-if="node.accessAddrs != null && node.accessAddrs.length > 0">

View File

@@ -1,7 +1,7 @@
Tea.context(function () { Tea.context(function () {
// 创建节点 // 创建节点
this.createNode = function () { this.createNode = function () {
teaweb.popup("/api/node/createPopup", { teaweb.popup(".node.createPopup", {
width: "50em", width: "50em",
height: "30em", height: "30em",
callback: function () { callback: function () {
@@ -16,11 +16,20 @@ Tea.context(function () {
this.deleteNode = function (nodeId) { this.deleteNode = function (nodeId) {
let that = this let that = this
teaweb.confirm("确定要删除此节点吗?", function () { teaweb.confirm("确定要删除此节点吗?", function () {
that.$post("/api/delete") that.$post(".delete")
.params({ .params({
nodeId: nodeId nodeId: nodeId
}) })
.refresh() .refresh()
}) })
} }
// 升级节点
this.upgradeNode = function (nodeId) {
teaweb.popup(".node.upgradePopup?nodeId=" + nodeId, {
onClose: function () {
teaweb.reload()
}
})
}
}) })

View File

@@ -0,0 +1,35 @@
{$layout "layout_popup"}
<h3>远程升级</h3>
<form class="ui form" data-tea-action="$" data-tea-success="success" data-tea-timeout="3600" data-tea-before="before" data-tea-done="done">
<csrf-token></csrf-token>
<input type="hidden" name="nodeId" :value="nodeId"/>
<table class="ui table definition selectable">
<tr v-show="nodeName.length > 0">
<td class="title">API节点</td>
<td>{{nodeName}}</td>
</tr>
<tr v-show="currentVersion.length > 0">
<td>当前版本</td>
<td>v{{currentVersion}}</td>
</tr>
<tr v-show="latestVersion.length > 0">
<td>目标版本</td>
<td>v{{latestVersion}}</td>
</tr>
<tr>
<td class="title">升级结果</td>
<td>
<span v-if="currentVersion == latestVersion" class="green">已经升级到最新版本</span>
<span :class="{red: !resultIsOk}" v-if="currentVersion != latestVersion">
<span v-if="!isRequesting">{{result}}</span>
<span v-if="isRequesting">升级中...</span>
</span>
</td>
</tr>
</table>
<submit-btn v-show="canUpgrade && !isRequesting && !isUpgrading">开始升级</submit-btn>
</form>

View File

@@ -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)
})
}
})