fix update mirror and make opt-in with --china-mirrors (#1035)

This commit is contained in:
henrygd
2025-08-29 13:42:30 -04:00
parent b084814aea
commit 94245a9ba4
6 changed files with 130 additions and 59 deletions

View File

@@ -4,12 +4,12 @@ import (
"beszel" "beszel"
"beszel/internal/agent" "beszel/internal/agent"
"beszel/internal/agent/health" "beszel/internal/agent/health"
"flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"strings" "strings"
"github.com/spf13/pflag"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@@ -17,43 +17,24 @@ import (
type cmdOptions struct { type cmdOptions struct {
key string // key is the public key(s) for SSH authentication. key string // key is the public key(s) for SSH authentication.
listen string // listen is the address or port to listen on. listen string // listen is the address or port to listen on.
// TODO: add hubURL and token
// hubURL string // hubURL is the URL of the hub to use.
// token string // token is the token to use for authentication.
} }
// parse parses the command line flags and populates the config struct. // parse parses the command line flags and populates the config struct.
// It returns true if a subcommand was handled and the program should exit. // It returns true if a subcommand was handled and the program should exit.
func (opts *cmdOptions) parse() bool { func (opts *cmdOptions) parse() bool {
flag.StringVar(&opts.key, "key", "", "Public key(s) for SSH authentication")
flag.StringVar(&opts.listen, "listen", "", "Address or port to listen on")
flag.Usage = func() {
builder := strings.Builder{}
builder.WriteString("Usage: ")
builder.WriteString(os.Args[0])
builder.WriteString(" [command] [flags]\n")
builder.WriteString("\nCommands:\n")
builder.WriteString(" health Check if the agent is running\n")
builder.WriteString(" help Display this help message\n")
builder.WriteString(" update Update to the latest version\n")
builder.WriteString("\nFlags:\n")
fmt.Print(builder.String())
flag.PrintDefaults()
}
subcommand := "" subcommand := ""
if len(os.Args) > 1 { if len(os.Args) > 1 {
subcommand = os.Args[1] subcommand = os.Args[1]
} }
// Subcommands that don't require any pflag parsing
switch subcommand { switch subcommand {
case "-v", "version": case "-v", "version":
fmt.Println(beszel.AppName+"-agent", beszel.Version) fmt.Println(beszel.AppName+"-agent", beszel.Version)
return true return true
case "help":
flag.Usage()
return true
case "update":
agent.Update()
return true
case "health": case "health":
err := health.Check() err := health.Check()
if err != nil { if err != nil {
@@ -63,7 +44,57 @@ func (opts *cmdOptions) parse() bool {
return true return true
} }
flag.Parse() // pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true
pflag.StringVarP(&opts.key, "key", "k", "", "Public key(s) for SSH authentication")
pflag.StringVarP(&opts.listen, "listen", "l", "", "Address or port to listen on")
// pflag.StringVarP(&opts.hubURL, "hub-url", "u", "", "URL of the hub to use")
// pflag.StringVarP(&opts.token, "token", "t", "", "Token to use for authentication")
chinaMirrors := pflag.BoolP("china-mirrors", "c", false, "Use mirror for update (gh.beszel.dev) instead of GitHub")
help := pflag.BoolP("help", "h", false, "Show this help message")
// Convert old single-dash long flags to double-dash for backward compatibility
flagsToConvert := []string{"key", "listen"}
for i, arg := range os.Args {
for _, flag := range flagsToConvert {
singleDash := "-" + flag
doubleDash := "--" + flag
if arg == singleDash {
os.Args[i] = doubleDash
break
} else if strings.HasPrefix(arg, singleDash+"=") {
os.Args[i] = doubleDash + arg[len(singleDash):]
break
}
}
}
pflag.Usage = func() {
builder := strings.Builder{}
builder.WriteString("Usage: ")
builder.WriteString(os.Args[0])
builder.WriteString(" [command] [flags]\n")
builder.WriteString("\nCommands:\n")
builder.WriteString(" health Check if the agent is running\n")
// builder.WriteString(" help Display this help message\n")
builder.WriteString(" update Update to the latest version\n")
builder.WriteString("\nFlags:\n")
fmt.Print(builder.String())
pflag.PrintDefaults()
}
// Parse all arguments with pflag
pflag.Parse()
// Must run after pflag.Parse()
switch {
case *help || subcommand == "help":
pflag.Usage()
return true
case subcommand == "update":
agent.Update(*chinaMirrors)
return true
}
return false return false
} }

View File

@@ -3,11 +3,11 @@ package main
import ( import (
"beszel/internal/agent" "beszel/internal/agent"
"crypto/ed25519" "crypto/ed25519"
"flag"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
@@ -245,7 +245,7 @@ func TestParseFlags(t *testing.T) {
oldArgs := os.Args oldArgs := os.Args
defer func() { defer func() {
os.Args = oldArgs os.Args = oldArgs
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
}() }()
tests := []struct { tests := []struct {
@@ -269,6 +269,22 @@ func TestParseFlags(t *testing.T) {
listen: "", listen: "",
}, },
}, },
{
name: "key flag double dash",
args: []string{"cmd", "--key", "testkey"},
expected: cmdOptions{
key: "testkey",
listen: "",
},
},
{
name: "key flag short",
args: []string{"cmd", "-k", "testkey"},
expected: cmdOptions{
key: "testkey",
listen: "",
},
},
{ {
name: "addr flag only", name: "addr flag only",
args: []string{"cmd", "-listen", ":8080"}, args: []string{"cmd", "-listen", ":8080"},
@@ -277,6 +293,22 @@ func TestParseFlags(t *testing.T) {
listen: ":8080", listen: ":8080",
}, },
}, },
{
name: "addr flag double dash",
args: []string{"cmd", "--listen", ":8080"},
expected: cmdOptions{
key: "",
listen: ":8080",
},
},
{
name: "addr flag short",
args: []string{"cmd", "-l", ":8080"},
expected: cmdOptions{
key: "",
listen: ":8080",
},
},
{ {
name: "both flags", name: "both flags",
args: []string{"cmd", "-key", "testkey", "-listen", ":8080"}, args: []string{"cmd", "-key", "testkey", "-listen", ":8080"},
@@ -290,12 +322,12 @@ func TestParseFlags(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Reset flags for each test // Reset flags for each test
flag.CommandLine = flag.NewFlagSet(tt.args[0], flag.ExitOnError) pflag.CommandLine = pflag.NewFlagSet(tt.args[0], pflag.ExitOnError)
os.Args = tt.args os.Args = tt.args
var opts cmdOptions var opts cmdOptions
opts.parse() opts.parse()
flag.Parse() pflag.Parse()
assert.Equal(t, tt.expected, opts) assert.Equal(t, tt.expected, opts)
}) })

View File

@@ -45,11 +45,13 @@ func getBaseApp() *pocketbase.PocketBase {
baseApp.RootCmd.Use = beszel.AppName baseApp.RootCmd.Use = beszel.AppName
baseApp.RootCmd.Short = "" baseApp.RootCmd.Short = ""
// add update command // add update command
baseApp.RootCmd.AddCommand(&cobra.Command{ updateCmd := &cobra.Command{
Use: "update", Use: "update",
Short: "Update " + beszel.AppName + " to the latest version", Short: "Update " + beszel.AppName + " to the latest version",
Run: hub.Update, Run: hub.Update,
}) }
updateCmd.Flags().Bool("china-mirrors", false, "Use mirror (gh.beszel.dev) instead of GitHub")
baseApp.RootCmd.AddCommand(updateCmd)
// add health command // add health command
baseApp.RootCmd.AddCommand(newHealthCmd()) baseApp.RootCmd.AddCommand(newHealthCmd())

View File

@@ -60,7 +60,7 @@ func detectRestarter() restarter {
// Update checks GitHub for a newer release of beszel-agent, applies it, // Update checks GitHub for a newer release of beszel-agent, applies it,
// fixes SELinux context if needed, and restarts the service. // fixes SELinux context if needed, and restarts the service.
func Update() error { func Update(useMirror bool) error {
exePath, _ := os.Executable() exePath, _ := os.Executable()
dataDir, err := getDataDir() dataDir, err := getDataDir()
@@ -70,6 +70,7 @@ func Update() error {
updated, err := ghupdate.Update(ghupdate.Config{ updated, err := ghupdate.Update(ghupdate.Config{
ArchiveExecutable: "beszel-agent", ArchiveExecutable: "beszel-agent",
DataDir: dataDir, DataDir: dataDir,
UseMirror: useMirror,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -99,6 +100,8 @@ func Update() error {
if err := r.Restart(); err != nil { if err := r.Restart(); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to restart service: %v", err) ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to restart service: %v", err)
ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually.") ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually.")
} else {
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
} }
} else { } else {
ghupdate.ColorPrint(ghupdate.ColorYellow, "No supported init system detected; please restart manually if needed.") ghupdate.ColorPrint(ghupdate.ColorYellow, "No supported init system detected; please restart manually if needed.")

View File

@@ -23,7 +23,7 @@ import (
const ( const (
colorReset = "\033[0m" colorReset = "\033[0m"
ColorYellow = "\033[33m" ColorYellow = "\033[33m"
colorGreen = "\033[32m" ColorGreen = "\033[32m"
colorCyan = "\033[36m" colorCyan = "\033[36m"
colorGray = "\033[90m" colorGray = "\033[90m"
) )
@@ -64,6 +64,10 @@ type Config struct {
// The data directory to use when fetching and downloading the latest release. // The data directory to use when fetching and downloading the latest release.
DataDir string DataDir string
// UseMirror specifies whether to use the beszel.dev mirror instead of GitHub API.
// When false (default), always uses api.github.com. When true, uses gh.beszel.dev.
UseMirror bool
} }
type updater struct { type updater struct {
@@ -106,21 +110,19 @@ func (p *updater) update() (updated bool, err error) {
var latest *release var latest *release
var useMirror bool var useMirror bool
// Determine the API endpoint based on UseMirror flag
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", p.config.Owner, p.config.Repo)
if p.config.UseMirror {
useMirror = true
apiURL = fmt.Sprintf("https://gh.beszel.dev/repos/%s/%s/releases/latest?api=true", p.config.Owner, p.config.Repo)
ColorPrint(ColorYellow, "Using mirror for update.")
}
latest, err = fetchLatestRelease( latest, err = fetchLatestRelease(
p.config.Context, p.config.Context,
p.config.HttpClient, p.config.HttpClient,
fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", p.config.Owner, p.config.Repo), apiURL,
) )
// if the first fetch fails, try the beszel.dev API (fallback for China)
if err != nil {
ColorPrint(ColorYellow, "Failed to fetch release. Trying beszel.dev mirror...")
useMirror = true
latest, err = fetchLatestRelease(
p.config.Context,
p.config.HttpClient,
fmt.Sprintf("https://gh.beszel.dev/repos/%s/%s/releases/latest?api=true", p.config.Owner, p.config.Repo),
)
}
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -129,7 +131,7 @@ func (p *updater) update() (updated bool, err error) {
newVersion := semver.MustParse(strings.TrimPrefix(latest.Tag, "v")) newVersion := semver.MustParse(strings.TrimPrefix(latest.Tag, "v"))
if newVersion.LTE(currentVersion) { if newVersion.LTE(currentVersion) {
ColorPrintf(colorGreen, "You already have the latest version %s.", p.currentVersion) ColorPrintf(ColorGreen, "You already have the latest version %s.", p.currentVersion)
return false, nil return false, nil
} }
@@ -209,14 +211,11 @@ func (p *updater) update() (updated bool, err error) {
} }
ColorPrint(colorGray, "---") ColorPrint(colorGray, "---")
ColorPrint(colorGreen, "Update completed successfully! You can start the executable as usual.") ColorPrint(ColorGreen, "Update completed successfully!")
// print the release notes // print the release notes
if latest.Body != "" { if latest.Body != "" {
fmt.Print("\n") fmt.Print("\n")
ColorPrintf(colorCyan, "Here is a list with some of the %s changes:", latest.Tag)
// remove the update command note to avoid "stuttering"
// (@todo consider moving to a config option)
releaseNotes := strings.TrimSpace(strings.Replace(latest.Body, "> _To update the prebuilt executable you can run `./"+p.config.ArchiveExecutable+" update`._", "", 1)) releaseNotes := strings.TrimSpace(strings.Replace(latest.Body, "> _To update the prebuilt executable you can run `./"+p.config.ArchiveExecutable+" update`._", "", 1))
ColorPrint(colorCyan, releaseNotes) ColorPrint(colorCyan, releaseNotes)
fmt.Print("\n") fmt.Print("\n")

View File

@@ -11,7 +11,7 @@ import (
) )
// Update updates beszel to the latest version // Update updates beszel to the latest version
func Update(_ *cobra.Command, _ []string) { func Update(cmd *cobra.Command, _ []string) {
dataDir := os.TempDir() dataDir := os.TempDir()
// set dataDir to ./beszel_data if it exists // set dataDir to ./beszel_data if it exists
@@ -19,9 +19,13 @@ func Update(_ *cobra.Command, _ []string) {
dataDir = "./beszel_data" dataDir = "./beszel_data"
} }
// Check if china-mirrors flag is set
useMirror, _ := cmd.Flags().GetBool("china-mirrors")
updated, err := ghupdate.Update(ghupdate.Config{ updated, err := ghupdate.Update(ghupdate.Config{
ArchiveExecutable: "beszel", ArchiveExecutable: "beszel",
DataDir: dataDir, DataDir: dataDir,
UseMirror: useMirror,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -49,13 +53,13 @@ func restartService() {
// Check if beszel service exists and is active // Check if beszel service exists and is active
cmd := exec.Command("systemctl", "is-active", "beszel.service") cmd := exec.Command("systemctl", "is-active", "beszel.service")
if err := cmd.Run(); err == nil { if err := cmd.Run(); err == nil {
fmt.Println("Restarting beszel service...") ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel service...")
restartCmd := exec.Command("systemctl", "restart", "beszel.service") restartCmd := exec.Command("systemctl", "restart", "beszel.service")
if err := restartCmd.Run(); err != nil { if err := restartCmd.Run(); err != nil {
fmt.Printf("Warning: Failed to restart service: %v\n", err) ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: Failed to restart service: %v\n", err)
fmt.Println("Please restart the service manually: sudo systemctl restart beszel") ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually: sudo systemctl restart beszel")
} else { } else {
fmt.Println("Service restarted successfully") ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
} }
return return
} }
@@ -65,17 +69,17 @@ func restartService() {
if _, err := exec.LookPath("rc-service"); err == nil { if _, err := exec.LookPath("rc-service"); err == nil {
cmd := exec.Command("rc-service", "beszel", "status") cmd := exec.Command("rc-service", "beszel", "status")
if err := cmd.Run(); err == nil { if err := cmd.Run(); err == nil {
fmt.Println("Restarting beszel service...") ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel service...")
restartCmd := exec.Command("rc-service", "beszel", "restart") restartCmd := exec.Command("rc-service", "beszel", "restart")
if err := restartCmd.Run(); err != nil { if err := restartCmd.Run(); err != nil {
fmt.Printf("Warning: Failed to restart service: %v\n", err) ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: Failed to restart service: %v\n", err)
fmt.Println("Please restart the service manually: sudo rc-service beszel restart") ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually: sudo rc-service beszel restart")
} else { } else {
fmt.Println("Service restarted successfully") ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
} }
return return
} }
} }
fmt.Println("Note: Service restart not attempted. If running as a service, restart manually.") ghupdate.ColorPrint(ghupdate.ColorYellow, "Service restart not attempted. If running as a service, restart manually.")
} }