Fleetd: Update the Registry DisplayVersion when fleetd auto-updates (#28183)

## For #27082 


### The Registry version string is updated with `fleetd`:


![ezgif-3881ff1bdfce04](https://github.com/user-attachments/assets/241009f5-91c0-4236-8526-95a3d2ce59e4)


- [x] Changes file added for user-visible changes in `orbit/changes/`
- [x] A detailed QA plan exists on the associated ticket
- [x] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [x] Make sure fleetd is compatible with the latest released version of
Fleet (see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/fleetd-development-and-release-strategy.md)).
- [x] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
jacobshandling
2025-04-17 11:04:02 -07:00
committed by GitHub
co-authored by Jacob Shandling
parent 8bbb296dba
commit 2c58f623bf
7 changed files with 138 additions and 17 deletions
+1 -1
View File
@@ -136,7 +136,7 @@ require (
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.27.0
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0
golang.org/x/sys v0.32.0
golang.org/x/term v0.30.0
golang.org/x/text v0.23.0
golang.org/x/tools v0.23.0
+2 -2
View File
@@ -1107,8 +1107,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -0,0 +1,2 @@
- When fleetd on a Windows host installs an update it detects from TUF, also update the
corresponding `DisplayVersion` in the Registry
+29 -12
View File
@@ -327,18 +327,29 @@ func (r *Runner) updateTarget(target string) error {
if err != nil {
return fmt.Errorf("get binary: %w", err)
}
// if this target is not orbit or osquery, do nothing else
if target != constant.OsqueryTUFTargetName && target != constant.OrbitTUFTargetName {
return nil
}
path := localTarget.ExecPath
newVersion, err := GetVersion(path)
if err != nil {
return fmt.Errorf("get new version from binary: %w", err)
}
if target == constant.OsqueryTUFTargetName {
// Compare old/new osquery versions
_, _ = compareVersion(path, r.OsqueryVersion, constant.OsqueryTUFTargetName)
_ = compareVersion(newVersion, r.OsqueryVersion, constant.OsqueryTUFTargetName)
}
if target != constant.OrbitTUFTargetName {
return nil
}
// target now guaranteed to be orbit
// Compare old/new orbit versions
_, _ = compareVersion(path, build.Version, "fleetd")
oVC := compareVersion(newVersion, build.Version, "fleetd")
// Symlink Orbit binary
linkPath := filepath.Join(r.updater.opt.RootDirectory, "bin", "orbit", filepath.Base(path))
@@ -350,6 +361,13 @@ func (r *Runner) updateTarget(target string) error {
return fmt.Errorf("symlink current: %w", err)
}
// oVC == 1 if upgrading or -1 if downgrading, 0 if the same version
if oVC != nil && *oVC != 0 && runtime.GOOS == "windows" {
if err := updateUninstallFleetdRegistryVersion(newVersion); err != nil {
return fmt.Errorf("update orbit version in Windows registry: %w", err)
}
}
return nil
}
@@ -358,12 +376,10 @@ func (r *Runner) Interrupt(err error) {
}
// compareVersion compares the old and new versions of a binary and prints the appropriate message.
// The return value is only used for unit tests.
func compareVersion(path string, oldVersion string, targetDisplayName string) (*int, error) {
newVersion, err := GetVersion(path)
if err != nil {
return nil, err
}
// The return value is used to determine whether to update the Windows registry and for unit tests.
func compareVersion(newVersion string, oldVersion string, targetDisplayName string) *int {
// this function is essentially a wrapper with logging of semver.IsValid and semver.Compare,
// neither of which return an error, so this doesn't return an error either
vOldVersion := "v" + oldVersion
vNewVersion := "v" + newVersion
if semver.IsValid(vOldVersion) && semver.IsValid(vNewVersion) {
@@ -376,9 +392,9 @@ func compareVersion(path string, oldVersion string, targetDisplayName string) (*
case -1:
log.Info().Msgf("Upgrading %s from %s to %s", targetDisplayName, oldVersion, newVersion)
}
return &compareResult, nil
return &compareResult
}
return nil, nil
return nil
}
// Matches strings like:
@@ -392,8 +408,9 @@ func GetVersion(path string) (string, error) {
versionCmd := exec.Command(path, "--version")
out, err := versionCmd.CombinedOutput()
if err != nil {
log.Warn().Msgf("failed to get %s version: %s: %s", path, string(out), err)
return "", err
wrappedErr := fmt.Errorf("failed to get %s version: %s: %s", path, string(out), err)
log.Warn().Msg(wrappedErr.Error())
return "", wrappedErr
}
matches := versionRegexp.FindStringSubmatch(strings.TrimSpace(string(out)))
if len(matches) > 2 {
+5 -2
View File
@@ -145,8 +145,9 @@ func TestGetVersion(t *testing.T) {
}
}
func TestCompareVersion(t *testing.T) {
func TestGetAndCompareVersion(t *testing.T) {
if runtime.GOOS == "windows" {
// windows filesystem writes require different cmd syntax
t.Skip("Skipping test on Windows")
}
t.Parallel()
@@ -208,9 +209,10 @@ func TestCompareVersion(t *testing.T) {
// "text file busy" is a Go issue when executing file just written: https://github.com/golang/go/issues/22315
var result *int
var newVersion string
retries := 0
for {
result, err = compareVersion(file.Name(), tc.oldVersion, "target")
newVersion, err = GetVersion(file.Name())
if err != nil {
t.Log(err)
if strings.Contains(err.Error(), "text file busy") {
@@ -227,6 +229,7 @@ func TestCompareVersion(t *testing.T) {
break
}
}
result = compareVersion(newVersion, tc.oldVersion, "target")
assert.Equal(t, tc.expected, result)
},
)
+92
View File
@@ -0,0 +1,92 @@
//go:build windows
package update
import (
"errors"
"fmt"
"golang.org/x/sys/windows/registry"
)
const (
REG_FLEETD_DISPLAY_NAME = "Fleet osquery"
// registry paths, absolute and relative to the HKEY_LOCAL_MACHINE root key - see
// https://pkg.go.dev/golang.org/x/sys/windows/registry#LOCAL_MACHINE and
// https://learn.microsoft.com/en-us/troubleshoot/windows-server/performance/windows-registry-advanced-users
HKEY_LOCAL_MACHINE_PATH = `Computer\HKEY_LOCAL_MACHINE`
REG_UNINSTALL_REL_PATH = `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`
REG_UNINSTALL_ABS_PATH = HKEY_LOCAL_MACHINE_PATH + `\` + REG_UNINSTALL_REL_PATH
)
func updateUninstallFleetdRegistryVersion(newVersion string) error {
// Since fleetd doesn't know its GUID key in the registry, iterate through all of them until we find
// the appropriate key
// path from the HKEY_LOCAL_MACHINE registry root key ("Computer\HKEY_LOCAL_MACHINE") to the key
// for the uninstall Fleetd registry entry. That is, REG_UNINSTALL_REL_PATH + `\` + (GUID of
// Fleetd entry ). This format is for compatibility with the registry.OpenKey function signature
uninstallFleetdRegRelPath, err := findUninstallFleetdRegKeyRelPath()
if err != nil {
return fmt.Errorf(`couldn't find the uninstall fleetd registry key in '%v': %w`, REG_UNINSTALL_ABS_PATH, err)
}
setKey, err := registry.OpenKey(registry.LOCAL_MACHINE, uninstallFleetdRegRelPath, registry.SET_VALUE)
if err != nil {
return fmt.Errorf(`couldn't open 'SET_VALUE' key handle for '%v\%v": %w`, HKEY_LOCAL_MACHINE_PATH, uninstallFleetdRegRelPath, err)
}
defer setKey.Close()
if err := setKey.SetStringValue("DisplayVersion", newVersion); err != nil {
return fmt.Errorf(`couldn't set value 'DisplayVersion' for '%v\%v: %w`, HKEY_LOCAL_MACHINE_PATH, uninstallFleetdRegRelPath, err)
}
return nil
}
func findUninstallFleetdRegKeyRelPath() (string, error) {
// get the existing keys in the Uninstall registry directory
enumerateKeyHandle, err := registry.OpenKey(registry.LOCAL_MACHINE, REG_UNINSTALL_REL_PATH, registry.READ)
if err != nil {
return "", fmt.Errorf(`couldn't open registry key '%v': %w`, REG_UNINSTALL_ABS_PATH, err)
}
defer enumerateKeyHandle.Close()
stat, err := enumerateKeyHandle.Stat()
if err != nil {
return "", fmt.Errorf(`couldn't get stat from registry key handle for '%v': %w`, REG_UNINSTALL_ABS_PATH, err)
}
subKeyCount := stat.SubKeyCount
keys, err := enumerateKeyHandle.ReadSubKeyNames(int(subKeyCount))
if err != nil {
return "", fmt.Errorf(`couldn't read subkeys of registry key handle for '%v': %w`, REG_UNINSTALL_ABS_PATH, err)
}
// find the Fleetd entry in the existing keys
var fleetdKey string
for _, key := range keys {
keyHandle, err := registry.OpenKey(registry.LOCAL_MACHINE, REG_UNINSTALL_REL_PATH+`\`+key, registry.READ)
if err != nil {
return "", fmt.Errorf(`couldn't open registry subkey handle for '%v\%v': %w`, REG_UNINSTALL_ABS_PATH, key, err)
}
defer keyHandle.Close()
displayName, _, err := keyHandle.GetStringValue("DisplayName")
if err != nil {
if errors.Is(err, registry.ErrNotExist) {
// this key doesn't have a `DisplayName`, so it's not the entry for Fleetd - keep looking
continue
}
return "", fmt.Errorf(`couldn't get registry string value 'DisplayName' for '%v\%v': %w`, REG_UNINSTALL_ABS_PATH, key, err)
}
if displayName == REG_FLEETD_DISPLAY_NAME {
fleetdKey = key
break
}
}
if fleetdKey == "" {
return "", fmt.Errorf(`couldn't find a corresponding registry value for fleetd in 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`)
}
return REG_UNINSTALL_REL_PATH + `\` + fleetdKey, nil
}
@@ -0,0 +1,7 @@
//go:build !windows
package update
func updateUninstallFleetdRegistryVersion(newVersion string) error {
return nil
}