mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
83a11ead2b
- Introduced OperationSetting component for managing agent configurations. - Updated settings page to include a new tab for operation settings. - Implemented functionality to fetch and update agent parameters such as heartbeat interval, sync interval, and auto-update settings. docs: enhance deployment documentation for agent installation - Added detailed instructions for agent installation using a script. - Included examples for using discovery and agent tokens. - Updated sections on global discovery tokens and agent auto-update features. docs: revise design and development guidelines for V3 - Updated design document to reflect the current state and goals for V3. - Clarified development guidelines to focus on operational experience improvements. ci: add GitHub Actions workflow for agent releases - Created a new workflow to automate the release of agent binaries on GitHub. - Configured the workflow to build binaries for multiple platforms and publish them as releases. feat: implement self-update mechanism for agent - Added updater module to handle checking for and applying updates from GitHub releases. - Implemented logic to restart the agent after a successful update. chore: create install script for agent deployment - Developed a bash script to facilitate the installation of the ATSFlare agent. - The script supports automatic configuration and systemd service creation.
196 lines
4.4 KiB
Go
196 lines
4.4 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"atsflare-agent/internal/config"
|
|
)
|
|
|
|
type Service struct {
|
|
httpClient *http.Client
|
|
lastCheckTag string
|
|
}
|
|
|
|
func New() *Service {
|
|
return &Service{
|
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
type githubRelease struct {
|
|
TagName string `json:"tag_name"`
|
|
Assets []githubAsset `json:"assets"`
|
|
}
|
|
|
|
type githubAsset struct {
|
|
Name string `json:"name"`
|
|
BrowserDownloadURL string `json:"browser_download_url"`
|
|
}
|
|
|
|
func (s *Service) CheckAndUpdate(ctx context.Context, repo string) error {
|
|
release, err := s.getLatestRelease(ctx, repo)
|
|
if err != nil {
|
|
return fmt.Errorf("check latest release: %w", err)
|
|
}
|
|
if release == nil || release.TagName == "" {
|
|
return nil
|
|
}
|
|
|
|
remoteVersion := normalizeVersion(release.TagName)
|
|
localVersion := normalizeVersion(config.AgentVersion)
|
|
|
|
if remoteVersion == localVersion || remoteVersion == s.lastCheckTag {
|
|
return nil
|
|
}
|
|
if !isNewer(localVersion, remoteVersion) {
|
|
s.lastCheckTag = remoteVersion
|
|
return nil
|
|
}
|
|
|
|
log.Printf("agent update available: %s -> %s", localVersion, remoteVersion)
|
|
assetName := fmt.Sprintf("atsflare-agent-%s-%s", runtime.GOOS, runtime.GOARCH)
|
|
|
|
var downloadURL string
|
|
for _, asset := range release.Assets {
|
|
if asset.Name == assetName {
|
|
downloadURL = asset.BrowserDownloadURL
|
|
break
|
|
}
|
|
}
|
|
if downloadURL == "" {
|
|
s.lastCheckTag = remoteVersion
|
|
return fmt.Errorf("no matching asset %q in release %s", assetName, release.TagName)
|
|
}
|
|
|
|
execPath, err := os.Executable()
|
|
if err != nil {
|
|
return fmt.Errorf("get executable path: %w", err)
|
|
}
|
|
if err = s.downloadAndReplace(ctx, downloadURL, execPath); err != nil {
|
|
return fmt.Errorf("download and replace: %w", err)
|
|
}
|
|
|
|
log.Printf("agent binary updated, restarting...")
|
|
return s.restart(execPath)
|
|
}
|
|
|
|
func (s *Service) getLatestRelease(ctx context.Context, repo string) (*githubRelease, error) {
|
|
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, nil
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("github api returned %s", resp.Status)
|
|
}
|
|
|
|
var release githubRelease
|
|
if err = json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
|
return nil, err
|
|
}
|
|
return &release, nil
|
|
}
|
|
|
|
func (s *Service) downloadAndReplace(ctx context.Context, url string, targetPath string) error {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("download returned %s", resp.Status)
|
|
}
|
|
|
|
tmpPath := targetPath + ".update"
|
|
tmpFile, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = io.Copy(tmpFile, resp.Body); err != nil {
|
|
tmpFile.Close()
|
|
os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
tmpFile.Close()
|
|
|
|
backupPath := targetPath + ".bak"
|
|
os.Remove(backupPath)
|
|
if err = os.Rename(targetPath, backupPath); err != nil {
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("backup current binary: %w", err)
|
|
}
|
|
if err = os.Rename(tmpPath, targetPath); err != nil {
|
|
// Attempt to restore backup
|
|
os.Rename(backupPath, targetPath)
|
|
return fmt.Errorf("replace binary: %w", err)
|
|
}
|
|
os.Remove(backupPath)
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) restart(execPath string) error {
|
|
argv := os.Args
|
|
if err := syscall.Exec(execPath, argv, os.Environ()); err != nil {
|
|
return fmt.Errorf("exec restart: %w", err)
|
|
}
|
|
return errors.New("unreachable after exec")
|
|
}
|
|
|
|
func normalizeVersion(v string) string {
|
|
v = strings.TrimSpace(v)
|
|
v = strings.TrimPrefix(v, "v")
|
|
return v
|
|
}
|
|
|
|
func isNewer(local, remote string) bool {
|
|
localParts := strings.Split(local, ".")
|
|
remoteParts := strings.Split(remote, ".")
|
|
maxLen := len(localParts)
|
|
if len(remoteParts) > maxLen {
|
|
maxLen = len(remoteParts)
|
|
}
|
|
for i := 0; i < maxLen; i++ {
|
|
lp, rp := "0", "0"
|
|
if i < len(localParts) {
|
|
lp = localParts[i]
|
|
}
|
|
if i < len(remoteParts) {
|
|
rp = remoteParts[i]
|
|
}
|
|
if rp > lp {
|
|
return true
|
|
}
|
|
if rp < lp {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|