Files
beszel/beszel/internal/agent/update.go
2025-08-25 17:30:42 -04:00

168 lines
5.2 KiB
Go

package agent
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
"beszel"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
)
// restarter knows how to restart the beszel-agent service.
type restarter interface {
Restart() error
}
type systemdRestarter struct{ cmd string }
func (s *systemdRestarter) Restart() error {
// Only restart if the service is active
if err := exec.Command(s.cmd, "is-active", "beszel-agent.service").Run(); err != nil {
return nil
}
log.Print("Restarting beszel-agent.service via systemd…")
return exec.Command(s.cmd, "restart", "beszel-agent.service").Run()
}
type openRCRestarter struct{ cmd string }
func (o *openRCRestarter) Restart() error {
if err := exec.Command(o.cmd, "status", "beszel-agent").Run(); err != nil {
return nil
}
log.Print("Restarting beszel-agent via OpenRC…")
return exec.Command(o.cmd, "restart", "beszel-agent").Run()
}
type openWRTRestarter struct{ cmd string }
func (w *openWRTRestarter) Restart() error {
if err := exec.Command(w.cmd, "running", "beszel-agent").Run(); err != nil {
return nil
}
log.Print("Restarting beszel-agent via procd…")
return exec.Command(w.cmd, "restart", "beszel-agent").Run()
}
func detectRestarter() restarter {
if path, err := exec.LookPath("systemctl"); err == nil {
return &systemdRestarter{cmd: path}
}
if path, err := exec.LookPath("rc-service"); err == nil {
return &openRCRestarter{cmd: path}
}
if path, err := exec.LookPath("service"); err == nil {
return &openWRTRestarter{cmd: path}
}
return nil
}
// Update checks GitHub for a newer release of beszel-agent, applies it,
// fixes SELinux context if needed, and restarts the service.
func Update() error {
// 1) Parse current version
current, err := semver.Parse(beszel.Version)
if err != nil {
return fmt.Errorf("invalid current version %q: %w", beszel.Version, err)
}
log.Printf("Current version: %s", current)
// 2) Create updater with our binary name filter
updater, err := selfupdate.NewUpdater(selfupdate.Config{
Filters: []string{"beszel-agent"},
})
if err != nil {
return fmt.Errorf("creating self-update client: %w", err)
}
// 3) Detect latest
log.Print("Checking for updates…")
latest, found, err := updater.DetectLatest("henrygd/beszel")
if err != nil {
return fmt.Errorf("failed to detect latest release: %w", err)
}
if !found {
log.Print("No updates available.")
return nil
}
log.Printf("Latest version: %s", latest.Version)
// 4) Compare versions
if !latest.Version.GT(current) {
log.Print("You are already up to date.")
return nil
}
// 5) Perform the update
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("unable to locate executable: %w", err)
}
log.Printf("Updating from %s to %s…", current, latest.Version)
if err := updater.UpdateTo(latest, exePath); err != nil {
return fmt.Errorf("update failed: %w", err)
}
log.Printf("Successfully updated to %s", latest.Version)
log.Print("Release notes:\n", strings.TrimSpace(latest.ReleaseNotes))
// 6) Fix SELinux context if necessary
if err := handleSELinuxContext(exePath); err != nil {
log.Printf("Warning: SELinux context handling: %v", err)
}
// 7) Restart service if running under a recognised init system
if r := detectRestarter(); r != nil {
if err := r.Restart(); err != nil {
log.Printf("Warning: failed to restart service: %v", err)
log.Print("Please restart the service manually.")
}
} else {
log.Print("No supported init system detected; please restart manually if needed.")
}
return nil
}
// handleSELinuxContext restores or applies the correct SELinux label to the binary.
func handleSELinuxContext(path string) error {
out, err := exec.Command("getenforce").Output()
if err != nil {
// SELinux not enabled or getenforce not available
return nil
}
state := strings.TrimSpace(string(out))
if state == "Disabled" {
return nil
}
log.Print("SELinux is enabled; applying context…")
var errs []string
// Try persistent context via semanage+restorecon
if semanagePath, err := exec.LookPath("semanage"); err == nil {
if err := exec.Command(semanagePath, "fcontext", "-a", "-t", "bin_t", path).Run(); err != nil {
errs = append(errs, "semanage fcontext failed: "+err.Error())
} else if restoreconPath, err := exec.LookPath("restorecon"); err == nil {
if err := exec.Command(restoreconPath, "-v", path).Run(); err != nil {
errs = append(errs, "restorecon failed: "+err.Error())
}
}
}
// Fallback to temporary context via chcon
if chconPath, err := exec.LookPath("chcon"); err == nil {
if err := exec.Command(chconPath, "-t", "bin_t", path).Run(); err != nil {
errs = append(errs, "chcon failed: "+err.Error())
}
}
if len(errs) > 0 {
return fmt.Errorf("SELinux context errors: %s", strings.Join(errs, "; "))
}
return nil
}