Fleetd: Update the Registry DisplayVersion when fleetd auto-updates (#28183)
## For #27082 ### The Registry version string is updated with `fleetd`:  - [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:
co-authored by
Jacob Shandling
parent
8bbb296dba
commit
2c58f623bf
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user