Files
pulse/internal/updates/version.go
T
Pulse Monitor 9bc050371b fix: Docker persistence actually fixed this time
The ConfigHandlers were calling config.SaveConfig() which uses
globalPersistence initialized at startup with potentially wrong path.
Now handlers use their own persistence instance directly which is
initialized with the correct DataPath.

This was causing Docker configurations to still save to /etc/pulse
even though we thought we fixed it in v4.0.6.

Fixes #253 (for real this time)
2025-08-06 17:14:34 +00:00

202 lines
4.6 KiB
Go

package updates
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
)
// Version represents a semantic version
type Version struct {
Major int
Minor int
Patch int
Prerelease string
Build string
}
// VersionInfo contains detailed version information
type VersionInfo struct {
Version string `json:"version"`
Build string `json:"build"`
Runtime string `json:"runtime"`
Channel string `json:"channel,omitempty"`
IsDocker bool `json:"isDocker"`
IsDevelopment bool `json:"isDevelopment"`
}
// ParseVersion parses a version string into a Version struct
func ParseVersion(versionStr string) (*Version, error) {
// Remove 'v' prefix if present
versionStr = strings.TrimPrefix(versionStr, "v")
// Regular expression for semantic versioning
re := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+(.+))?$`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) == 0 {
return nil, fmt.Errorf("invalid version format: %s", versionStr)
}
major, _ := strconv.Atoi(matches[1])
minor, _ := strconv.Atoi(matches[2])
patch, _ := strconv.Atoi(matches[3])
return &Version{
Major: major,
Minor: minor,
Patch: patch,
Prerelease: matches[4],
Build: matches[5],
}, nil
}
// String returns the string representation of the version
func (v *Version) String() string {
version := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
if v.Prerelease != "" {
version += "-" + v.Prerelease
}
if v.Build != "" {
version += "+" + v.Build
}
return version
}
// Compare compares two versions
// Returns:
// -1 if v < other
// 0 if v == other
// 1 if v > other
func (v *Version) Compare(other *Version) int {
if v.Major != other.Major {
return compareInts(v.Major, other.Major)
}
if v.Minor != other.Minor {
return compareInts(v.Minor, other.Minor)
}
if v.Patch != other.Patch {
return compareInts(v.Patch, other.Patch)
}
// Handle prerelease versions
if v.Prerelease == "" && other.Prerelease != "" {
return 1 // v is release, other is prerelease
}
if v.Prerelease != "" && other.Prerelease == "" {
return -1 // v is prerelease, other is release
}
if v.Prerelease != other.Prerelease {
return strings.Compare(v.Prerelease, other.Prerelease)
}
return 0
}
// IsNewerThan returns true if v is newer than other
func (v *Version) IsNewerThan(other *Version) bool {
return v.Compare(other) > 0
}
// IsPrerelease returns true if this is a prerelease version
func (v *Version) IsPrerelease() bool {
return v.Prerelease != ""
}
// GetCurrentVersion gets the current running version
func GetCurrentVersion() (*VersionInfo, error) {
// Try to get version from git first (development)
gitVersion, err := getGitVersion()
if err == nil && gitVersion != "" {
return &VersionInfo{
Version: gitVersion,
Build: "development",
Runtime: "go",
IsDevelopment: true,
IsDocker: isDockerEnvironment(),
}, nil
}
// Try to read from VERSION file in multiple locations
versionPaths := []string{
"VERSION",
"/opt/pulse/VERSION",
filepath.Join(filepath.Dir(os.Args[0]), "VERSION"),
}
for _, path := range versionPaths {
versionBytes, err := os.ReadFile(path)
if err == nil {
return &VersionInfo{
Version: strings.TrimSpace(string(versionBytes)),
Build: "release",
Runtime: "go",
IsDevelopment: false,
IsDocker: isDockerEnvironment(),
}, nil
}
}
// Final fallback
return &VersionInfo{
Version: "4.0.7",
Build: "release",
Runtime: "go",
IsDevelopment: false,
IsDocker: isDockerEnvironment(),
}, nil
}
// getGitVersion gets version information from git
func getGitVersion() (string, error) {
// Get the latest tag
cmd := exec.Command("git", "describe", "--tags", "--always", "--dirty")
output, err := cmd.Output()
if err != nil {
return "", err
}
version := strings.TrimSpace(string(output))
if version == "" {
return "", fmt.Errorf("no version found")
}
return version, nil
}
// isDockerEnvironment checks if running in Docker
func isDockerEnvironment() bool {
// Check for Docker-specific files
if fileExists("/.dockerenv") {
return true
}
// Check cgroup for Docker
data, err := exec.Command("cat", "/proc/1/cgroup").Output()
if err == nil && strings.Contains(string(data), "docker") {
return true
}
return false
}
// fileExists checks if a file exists
func fileExists(path string) bool {
cmd := exec.Command("test", "-f", path)
return cmd.Run() == nil
}
// compareInts compares two integers
func compareInts(a, b int) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}