mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Issue 10300 self healing (#10335)
This relates to #10300 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [X] Manual QA must be performed in the three main OSs, macOS, Windows, and Linux. - [X] Auto-update manual QA from the released version of the component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) --------- Co-authored-by: Lucas Rodriguez <lucas@fleetdm.com>
This commit is contained in:
parent
0b71e04e2e
commit
b15f2b877b
1
orbit/changes/10300-symlink-not-present-quirk
Normal file
1
orbit/changes/10300-symlink-not-present-quirk
Normal file
@ -0,0 +1 @@
|
||||
* An update bug where orbit symlink was not present is now fixed
|
@ -163,7 +163,6 @@ func main() {
|
||||
return fmt.Errorf("failed to set root-dir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
app.Action = func(c *cli.Context) error {
|
||||
@ -790,6 +789,10 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(os.Args) == 2 && os.Args[1] == "--help" {
|
||||
platform.PreUpdateQuirks()
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Error().Err(err).Msg("run orbit failed")
|
||||
}
|
||||
|
@ -89,3 +89,12 @@ func GetProcessByName(name string) (*gopsutil_process.Process, error) {
|
||||
func GetSMBiosUUID() (string, UUIDSource, error) {
|
||||
return "", UUIDSourceInvalid, errors.New("not implemented.")
|
||||
}
|
||||
|
||||
// RunUpdateQuirks is a no-op on non-windows platforms
|
||||
func PreUpdateQuirks() {
|
||||
}
|
||||
|
||||
// IsInvalidReparsePoint is a no-op on non-windows platforms
|
||||
func IsInvalidReparsePoint(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ package platform
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@ -15,6 +17,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
||||
|
||||
"github.com/digitalocean/go-smbios/smbios"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hectane/go-acl"
|
||||
gopsutil_process "github.com/shirou/gopsutil/v3/process"
|
||||
"golang.org/x/sys/windows"
|
||||
@ -341,3 +344,205 @@ func GetSMBiosUUID() (string, UUIDSource, error) {
|
||||
// UUID was obtained from calling WMI infrastructure
|
||||
return uuid, UUIDSourceWMI, nil
|
||||
}
|
||||
|
||||
// getExecutablePath returns the current working directory
|
||||
func getExecutablePath() (string, error) {
|
||||
// getting current executable fullpath
|
||||
exec, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// returns the current executable directory
|
||||
return filepath.Dir(exec), nil
|
||||
}
|
||||
|
||||
// getOrbitVersion returns the version of the Orbit executable
|
||||
func getOrbitVersion(path string) (string, error) {
|
||||
const (
|
||||
expectedPrefix = "orbit "
|
||||
expectedVersionFlag = "-version"
|
||||
)
|
||||
|
||||
if len(path) == 0 {
|
||||
return "", errors.New("input executable is empty")
|
||||
}
|
||||
|
||||
// running the executable with the version flag
|
||||
args := []string{expectedVersionFlag}
|
||||
out, err := exec.Command(path, args...).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("there was a problem running target executable: %w", err)
|
||||
}
|
||||
|
||||
// parsing the output
|
||||
versionOutputStr := string(out)
|
||||
if len(versionOutputStr) == 0 {
|
||||
return "", errors.New("empty executable output")
|
||||
}
|
||||
|
||||
outputByLines := strings.Split(strings.TrimRight(versionOutputStr, "\n"), "\n")
|
||||
if len(outputByLines) < 1 {
|
||||
return "", errors.New("expected number of lines is not present")
|
||||
}
|
||||
|
||||
rawVersionStr := strings.TrimSpace(strings.ToLower(outputByLines[0]))
|
||||
if !strings.HasPrefix(rawVersionStr, expectedPrefix) {
|
||||
return "", errors.New("expected version prefix is not present")
|
||||
}
|
||||
|
||||
// getting the actual version string
|
||||
versionStr := strings.TrimPrefix(rawVersionStr, expectedPrefix)
|
||||
if len(versionStr) == 0 {
|
||||
return "", errors.New("expected version information is not present")
|
||||
}
|
||||
|
||||
return versionStr, nil
|
||||
}
|
||||
|
||||
// versionCheckForfixSymlinkNotPresentQuirk checks if the target orbit version has the problematic logic
|
||||
func versionCheckForfixSymlinkNotPresentQuirk(orbitPath string) error {
|
||||
// gathering target orbit version
|
||||
versionOrbit, err := getOrbitVersion(orbitPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting orbit version: %w", err)
|
||||
}
|
||||
|
||||
// checking if target orbit has the problematic logic
|
||||
if versionOrbit == "1.6.0" || versionOrbit == "1.7.0" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Orbit version does not have the problematic logic: %s", versionOrbit)
|
||||
}
|
||||
|
||||
// fixSymlinkNotPresent fixes the issue where the symlink to the orbit service binary is not present
|
||||
// this is a workaround for the issue described here https://github.com/fleetdm/fleet/issues/10300
|
||||
func fixSymlinkNotPresent() error {
|
||||
// getting current working directory
|
||||
execPath, err := getExecutablePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// getting the path to orbit service binary
|
||||
orbitPath := execPath + "\\..\\bin\\orbit\\orbit.exe"
|
||||
|
||||
// gathering target orbit version
|
||||
err = versionCheckForfixSymlinkNotPresentQuirk(orbitPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// checking if the orbit service binary symlink needs to be regenerated
|
||||
_, err = os.Readlink(orbitPath)
|
||||
|
||||
// if there are no errors or file is not present, there is nothing to do
|
||||
if err == nil || errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handling error by renaming the locked binary file, marking it for deletion on reboot and
|
||||
// regenerating the symlink
|
||||
|
||||
// We are now about to perform a sensitive operation
|
||||
|
||||
// renaming locked binary to a different file, the process will keep running, but it will be renamed
|
||||
// target orbit process is not terminated on purpose to avoid potential erros
|
||||
temporaryOrbitPath := orbitPath + "." + strings.ToUpper(uuid.New().String())
|
||||
|
||||
if err := os.Rename(orbitPath, temporaryOrbitPath); err != nil {
|
||||
return fmt.Errorf("rename: %w", err)
|
||||
}
|
||||
|
||||
// we need the symlink check to pass, so we are regenerating it to the newly renamed orbit binary.
|
||||
// We avoid using child directories here to reduce logic complexity.
|
||||
// The symlink is going to be regenerated and deleted during update process
|
||||
if err := os.Symlink(temporaryOrbitPath, orbitPath); err != nil {
|
||||
return fmt.Errorf("symlink current: %w", err)
|
||||
}
|
||||
|
||||
// the renamed binary file is locked because is used by a running process
|
||||
// so only thing possible is to mark it to be deleted upon reboot by using MOVEFILE_DELAY_UNTIL_REBOOT flag
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw
|
||||
if err := windows.MoveFileEx(windows.StringToUTF16Ptr(temporaryOrbitPath), nil, windows.MOVEFILE_DELAY_UNTIL_REBOOT); err != nil {
|
||||
return fmt.Errorf("movefileex: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isRunningAsSystem checks if the current process is running as SYSTEM
|
||||
func isRunningAsSystem() (bool, error) {
|
||||
// getting the current process token
|
||||
token, err := windows.OpenCurrentProcessToken()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer token.Close()
|
||||
|
||||
// getting the current process user
|
||||
user, err := token.GetTokenUser()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// checking if the current process user is SYSTEM
|
||||
if windows.EqualSid(user.User.Sid, constant.SystemSID) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// isRunningFromStagingDir checks if the current process is running from the staging directory
|
||||
func isRunningFromStagingDir() (bool, error) {
|
||||
// getting current working directory
|
||||
execPath, err := getExecutablePath()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// checking if the current executable directory is the staging directory and return error otherwise
|
||||
if !strings.HasSuffix(strings.ToLower(execPath), "staging") {
|
||||
return false, errors.New("not running from the staging directory")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// shouldQuirksRun determines if the software update quirks should be run
|
||||
// by checking if process is running as system and from staging directory
|
||||
// we can relax the constrains a bit if needed and just check for SYSTEM execution context
|
||||
func shouldQuirksRun() bool {
|
||||
isSystem, err := isRunningAsSystem()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
isStagingDir, err := isRunningFromStagingDir()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return isSystem && isStagingDir
|
||||
}
|
||||
|
||||
// PreUpdateQuirks runs the best-effort software update quirks
|
||||
// There is no logging support in this function as it is called
|
||||
// before the logging system is initialized.
|
||||
// Software quirks added here will be executed before an update.
|
||||
// Its main purpose is to fix issues that may prevent the update from being applied.
|
||||
// The quirks should be carefully reviewed and tested before being added.
|
||||
func PreUpdateQuirks() {
|
||||
if shouldQuirksRun() {
|
||||
// Fixing the symlink not present quirk
|
||||
// This is a best-effort fix, any error in fixSymlinkNotPresent is ignored
|
||||
fixSymlinkNotPresent()
|
||||
}
|
||||
}
|
||||
|
||||
// IsInvalidReparsePoint returns true if the error is ERROR_NOT_A_REPARSE_POINT
|
||||
func IsInvalidReparsePoint(err error) bool {
|
||||
return errors.Is(err, windows.ERROR_NOT_A_REPARSE_POINT)
|
||||
}
|
||||
|
@ -6,9 +6,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@ -199,9 +201,17 @@ func (r *Runner) UpdateAction() (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the hash of the repository is different than
|
||||
// that of the target local file.
|
||||
if !bytes.Equal(r.localHashes[target], metaHash) || needsSymlinkUpdate {
|
||||
// Check whether the hash of the repository is different than that of the target local file
|
||||
localBinaryNotUpdated := !bytes.Equal(r.localHashes[target], metaHash)
|
||||
|
||||
// Preventing the update of the symlink on Windows if the binary does not need to be updated
|
||||
if runtime.GOOS == "windows" && needsSymlinkUpdate && !localBinaryNotUpdated {
|
||||
needsSymlinkUpdate = false
|
||||
}
|
||||
|
||||
// Performing update if either the binary is not updated
|
||||
// or the symlink needs to be updated and binary is not updated.
|
||||
if localBinaryNotUpdated || needsSymlinkUpdate {
|
||||
// Update detected
|
||||
log.Info().Str("target", target).Msg("update detected")
|
||||
if err := r.updateTarget(target); err != nil {
|
||||
@ -232,6 +242,13 @@ func (r *Runner) needsOrbitSymlinkUpdate() (bool, error) {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if platform.IsInvalidReparsePoint(err) {
|
||||
// On Windows, the symlink may be a file instead of a symlink.
|
||||
// let's handle this case by forcing the update to happen
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("read existing symlink: %w", err)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user