mirror of
https://github.com/fleetdm/fleet
synced 2026-05-08 17:50:52 +00:00
Adding telemetry for catching issue #19172 # Docs changes In another PR: https://github.com/fleetdm/fleet/pull/23423/files # Demo <div> <a href="https://www.loom.com/share/233625875eec46508c26ae315cd52d19"> <p>[Demo] Add telemetry for vital fleetd errors - Issue #23413 - Watch Video</p> </a> <a href="https://www.loom.com/share/233625875eec46508c26ae315cd52d19"> <img style="max-width:300px;" src="https://cdn.loom.com/sessions/thumbnails/233625875eec46508c26ae315cd52d19-45ca0ec1b7b5e9e7-full-play.gif"> </a> </div> # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [x] Orbit runs on macOS, Linux and Windows. Check if the orbit feature/bugfix should only apply to one platform (`runtime.GOOS`). - [x] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [x] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).
265 lines
8.4 KiB
Go
265 lines
8.4 KiB
Go
package update
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
const (
|
|
nudgeConfigFile = "nudge-config.json"
|
|
nudgeConfigFileMode = os.FileMode(constant.DefaultWorldReadableFileMode)
|
|
)
|
|
|
|
// NudgeConfigReceiver is a kind of middleware that wraps an OrbitConfigFetcher and a Runner.
|
|
// It checks the config supplied by the wrapped OrbitConfigFetcher to detects whether the Fleet
|
|
// server has supplied a Nudge config. If so, it sets Nudge as a target on the wrapped Runner.
|
|
type NudgeConfigReceiver struct {
|
|
opt NudgeConfigFetcherOptions
|
|
// ensures only one command runs at a time, protects access to lastRun
|
|
cmdMu sync.Mutex
|
|
lastRun time.Time
|
|
|
|
// launchErr is set if Nudge fails to launch. If launchErr is set, we won't try to
|
|
// launch Nudge again.
|
|
launchErr *nudgeLaunchErr
|
|
}
|
|
|
|
type NudgeConfigFetcherOptions struct {
|
|
// UpdateRunner is the wrapped Runner where Nudge will be set as a target. It is responsible for
|
|
// actually ensuring that Nudge is installed and updated via the designated TUF server.
|
|
UpdateRunner *Runner
|
|
// RootDir is where the Nudge configuration will be stored
|
|
RootDir string
|
|
// Interval is the minimum amount of time that must pass to launch
|
|
// Nudge
|
|
Interval time.Duration
|
|
// runNudgeFn can be set in tests to mock the command executed to
|
|
// run Nudge.
|
|
runNudgeFn func(execPath, configPath string) error
|
|
}
|
|
|
|
func ApplyNudgeConfigReceiverMiddleware(opt NudgeConfigFetcherOptions) fleet.OrbitConfigReceiver {
|
|
return &NudgeConfigReceiver{opt: opt}
|
|
}
|
|
|
|
// GetConfig calls the wrapped Fetcher's GetConfig method, and detects if the
|
|
// Fleet server has supplied a Nudge config.
|
|
//
|
|
// If a Nudge config is supplied, it:
|
|
//
|
|
// - ensures that Nudge is installed and updated via the designated TUF server.
|
|
// - ensures that Nudge is opened at an interval given by n.frequency with the
|
|
// provided config.
|
|
func (n *NudgeConfigReceiver) Run(cfg *fleet.OrbitConfig) error {
|
|
log.Debug().Msg("running nudge config fetcher middleware")
|
|
|
|
if cfg == nil {
|
|
log.Debug().Msg("NudgeConfigReceiver received nil config")
|
|
return nil
|
|
}
|
|
|
|
if n.opt.UpdateRunner == nil {
|
|
log.Debug().Msg("NudgeConfigReceiver received nil UpdateRunner, this probably indicates that updates are turned off. Skipping any actions related to Nudge")
|
|
return nil
|
|
}
|
|
|
|
if cfg.NudgeConfig == nil {
|
|
log.Debug().Msg("empty nudge config, removing nudge as target")
|
|
// TODO(roberto): by early returning and removing the target from the
|
|
// runner/updater we ensure Nudge won't be opened/updated again
|
|
// but we don't actually remove the file from disk. We
|
|
// knowingly decided to do this as a post MVP optimization.
|
|
n.opt.UpdateRunner.RemoveRunnerOptTarget("nudge")
|
|
n.opt.UpdateRunner.updater.RemoveTargetInfo("nudge")
|
|
return nil
|
|
}
|
|
|
|
updaterHasTarget := n.opt.UpdateRunner.HasRunnerOptTarget("nudge")
|
|
runnerHasLocalHash := n.opt.UpdateRunner.HasLocalHash("nudge")
|
|
if !updaterHasTarget || !runnerHasLocalHash {
|
|
log.Info().Msg("refreshing the update runner config with Nudge targets and hashes")
|
|
log.Debug().Msgf("updater has target: %t, runner has local hash: %t", updaterHasTarget, runnerHasLocalHash)
|
|
return n.setTargetsAndHashes()
|
|
}
|
|
|
|
if err := n.configure(*cfg.NudgeConfig); err != nil {
|
|
log.Info().Err(err).Msg("nudge configuration")
|
|
return err
|
|
}
|
|
|
|
if err := n.launch(); err != nil {
|
|
log.Info().Err(err).Msg("nudge launch")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *NudgeConfigReceiver) setTargetsAndHashes() error {
|
|
n.opt.UpdateRunner.AddRunnerOptTarget("nudge")
|
|
n.opt.UpdateRunner.updater.SetTargetInfo("nudge", NudgeMacOSTarget)
|
|
// we don't want to keep nudge as a target if we failed to update the
|
|
// cached hashes in the runner.
|
|
if err := n.opt.UpdateRunner.StoreLocalHash("nudge"); err != nil {
|
|
log.Debug().Msgf("removing nudge from target options, error updating local hashes: %s", err)
|
|
n.opt.UpdateRunner.RemoveRunnerOptTarget("nudge")
|
|
n.opt.UpdateRunner.updater.RemoveTargetInfo("nudge")
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *NudgeConfigReceiver) configure(nudgeCfg fleet.NudgeConfig) error {
|
|
jsonCfg, err := json.Marshal(nudgeCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfgFile := filepath.Join(n.opt.RootDir, nudgeConfigFile)
|
|
writeConfig := func() error {
|
|
return os.WriteFile(cfgFile, jsonCfg, constant.DefaultWorldReadableFileMode)
|
|
}
|
|
|
|
fileInfo, err := os.Stat(cfgFile)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return writeConfig()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// ensure the config file has the right permissions, a call to
|
|
// WriteFile preserves existing permissions if the file already exists,
|
|
// and previous versions of orbit set the permissions of this file to
|
|
// constant.DefaultFileMode.
|
|
if fileInfo.Mode() != nudgeConfigFileMode {
|
|
log.Info().Msgf("%s config file had wrong permissions (%v) setting permissions to %v", cfgFile, fileInfo.Mode(), nudgeConfigFileMode)
|
|
if err := os.Chmod(cfgFile, nudgeConfigFileMode); err != nil {
|
|
return fmt.Errorf("ensuring permissions of config file, chmod %q: %w", cfgFile, err)
|
|
}
|
|
}
|
|
|
|
// this not only an optimization, but mostly a safeguard: if the file
|
|
// has been tampered and contains very large contents, we don't
|
|
// want to load them into memory.
|
|
if fileInfo.Size() != int64(len(jsonCfg)) {
|
|
log.Debug().Msg("configuring nudge: local file has different size than remote, writing remote config")
|
|
return writeConfig()
|
|
}
|
|
|
|
fileBytes, err := os.ReadFile(cfgFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !bytes.Equal(fileBytes, jsonCfg) {
|
|
log.Debug().Msg("configuring nudge: local file is different than remote, writing remote config")
|
|
return writeConfig()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *NudgeConfigReceiver) launch() error {
|
|
cfgFile := filepath.Join(n.opt.RootDir, nudgeConfigFile)
|
|
|
|
if n.cmdMu.TryLock() {
|
|
defer n.cmdMu.Unlock()
|
|
|
|
if time.Since(n.lastRun) > n.opt.Interval {
|
|
nudge, err := n.opt.UpdateRunner.updater.localTarget("nudge")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// before moving forward, check that the file at the
|
|
// path is the file we're about to open hasn't been
|
|
// tampered with.
|
|
meta, err := n.opt.UpdateRunner.updater.Lookup("nudge")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// if we can't find the file, or the hash doesn't match
|
|
// make sure nudge is added as a target and the hashes
|
|
// are refreshed
|
|
if err := checkFileHash(meta, nudge.Path); err != nil {
|
|
n.launchErr = nil // reset launchErr since we're dealing with a different file
|
|
return n.setTargetsAndHashes()
|
|
}
|
|
|
|
// if we have a prior launch error, we won't try to launch nudge again
|
|
if n.launchErr != nil {
|
|
log.Info().Msgf("Nudge disabled since %s due to launch error: %v", n.launchErr.timestamp.Format("2006-01-02"), n.launchErr)
|
|
n.lastRun = time.Now()
|
|
return nil
|
|
}
|
|
|
|
fn := n.opt.runNudgeFn
|
|
if fn == nil {
|
|
fn = func(appPath, configPath string) error {
|
|
// TODO(roberto): when an user selects "Later" from the
|
|
// Nudge defer menu, the Nudge UI will be shown the
|
|
// next time Nudge is launched. If for some reason orbit
|
|
// restarts (eg: an update) and the user has a pending
|
|
// OS update, we might show Nudge more than one time
|
|
// every n.frequency.
|
|
//
|
|
// Note that this only happens for the "Later" option,
|
|
// all other options behave as expected and Nudge will
|
|
// respect the time chosen (eg: next day) and it won't
|
|
// show up even if it's opened multiple times in that
|
|
// interval.
|
|
log.Info().Msg("running Nudge")
|
|
_, err := execuser.Run(
|
|
appPath,
|
|
execuser.WithArg("-json-url", configPath),
|
|
)
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := fn(nudge.DirPath, fmt.Sprintf("file://%s", cfgFile)); err != nil {
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
launchErr := &nudgeLaunchErr{
|
|
err: err,
|
|
exitCode: exitErr.ExitCode(),
|
|
detail: string(exitErr.Stderr),
|
|
cfgFile: cfgFile,
|
|
timestamp: time.Now(),
|
|
}
|
|
n.launchErr = launchErr
|
|
return fmt.Errorf("opening Nudge with config %q: %w", cfgFile, launchErr)
|
|
}
|
|
return fmt.Errorf("opening Nudge with config %q: %w", cfgFile, err)
|
|
}
|
|
|
|
n.lastRun = time.Now()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type nudgeLaunchErr struct {
|
|
err error
|
|
exitCode int
|
|
detail string
|
|
cfgFile string
|
|
timestamp time.Time
|
|
}
|
|
|
|
func (e *nudgeLaunchErr) Error() string {
|
|
return fmt.Sprintf("%v: %s", e.err, e.detail)
|
|
}
|