mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Remotely configure fleetd update channels (#15848)
#13825 - [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] Added/updated tests - [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 released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). --------- Co-authored-by: Victor Lyuboslavsky <victor.lyuboslavsky@gmail.com>
This commit is contained in:
parent
38b8c9cc58
commit
d2015d1a36
1
changes/13825-remotely-configure-fleetd-update-channels
Normal file
1
changes/13825-remotely-configure-fleetd-update-channels
Normal file
@ -0,0 +1 @@
|
||||
* Remotely configure fleetd update channels in agent options (Fleet Premium only, and requires fleetd >= 1.20.0).
|
@ -0,0 +1,2 @@
|
||||
* Allow configuring TUF channels of `orbit`, `osqueryd` and `desktop` from Fleet agent settings.
|
||||
* Add `uptime` column to `orbit_info` table.
|
@ -213,6 +213,7 @@ func main() {
|
||||
fmt.Println("orbit " + build.Version)
|
||||
return nil
|
||||
}
|
||||
startTime := time.Now()
|
||||
|
||||
var logFile io.Writer
|
||||
if logf := c.String("log-file"); logf != "" {
|
||||
@ -247,6 +248,12 @@ func main() {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
|
||||
// Override flags with values retrieved from Fleet.
|
||||
fallbackServerOverridesCfg := setServerOverrides(c)
|
||||
if !fallbackServerOverridesCfg.empty() {
|
||||
log.Debug().Msgf("fallback settings: %+v", fallbackServerOverridesCfg)
|
||||
}
|
||||
|
||||
if c.Bool("insecure") && c.String("fleet-certificate") != "" {
|
||||
return errors.New("insecure and fleet-certificate may not be specified together")
|
||||
}
|
||||
@ -433,25 +440,11 @@ func main() {
|
||||
// restart. This was changed to have control over
|
||||
// how/when we want to retry to download the packages.
|
||||
err = retrypkg.Do(func() error {
|
||||
osquerydLocalTarget, err := updater.Get("osqueryd")
|
||||
var err error
|
||||
osquerydPath, desktopPath, err = getFleetdComponentPaths(c, updater, fallbackServerOverridesCfg)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("get osqueryd target failed")
|
||||
return fmt.Errorf("get osqueryd target: %w", err)
|
||||
return err
|
||||
}
|
||||
osquerydPath = osquerydLocalTarget.ExecPath
|
||||
if c.Bool("fleet-desktop") {
|
||||
fleetDesktopLocalTarget, err := updater.Get("desktop")
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("get desktop target failed")
|
||||
return fmt.Errorf("get desktop target: %w", err)
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
desktopPath = fleetDesktopLocalTarget.DirPath
|
||||
} else {
|
||||
desktopPath = fleetDesktopLocalTarget.ExecPath
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
// retry every 5 minutes to not flood the logs,
|
||||
@ -724,6 +717,32 @@ func main() {
|
||||
}
|
||||
g.Add(flagRunner.Execute, flagRunner.Interrupt)
|
||||
|
||||
if !c.Bool("disable-updates") {
|
||||
const serverOverridesInterval = 30 * time.Second
|
||||
serverOverridesRunner := newServerOverridesRunner(
|
||||
configFetcher,
|
||||
c.String("root-dir"),
|
||||
serverOverridesInterval,
|
||||
fallbackServerOverridesConfig{
|
||||
OsquerydPath: osquerydPath,
|
||||
DesktopPath: desktopPath,
|
||||
},
|
||||
c.Bool("fleet-desktop"),
|
||||
)
|
||||
// Perform initial run to update overrides as soon as possible.
|
||||
didUpdate, err := serverOverridesRunner.run()
|
||||
if err != nil {
|
||||
// Just log, OK to continue, since serverOverridesRunner will retry
|
||||
// in serverOverridesRunner.Execute.
|
||||
log.Debug().Err(err).Msg("initial flags update failed")
|
||||
}
|
||||
if didUpdate {
|
||||
log.Info().Msg("exiting due to early update of server overrides")
|
||||
return nil
|
||||
}
|
||||
g.Add(serverOverridesRunner.Execute, serverOverridesRunner.Interrupt)
|
||||
}
|
||||
|
||||
// only setup extensions autoupdate if we have enabled updates
|
||||
// for extensions autoupdate, we can only proceed after orbit is enrolled in fleet
|
||||
// and all relevant things for it (like certs, enroll secrets, tls proxy, etc) is configured
|
||||
@ -943,6 +962,7 @@ func main() {
|
||||
c.String("osqueryd-channel"),
|
||||
c.String("desktop-channel"),
|
||||
trw,
|
||||
startTime,
|
||||
)),
|
||||
)
|
||||
|
||||
@ -1022,6 +1042,79 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// setServerOverrides overrides specific variables in c with values fetched from Fleet.
|
||||
func setServerOverrides(c *cli.Context) fallbackServerOverridesConfig {
|
||||
overrideCfg, err := loadServerOverrides(c.String("root-dir"))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load server overrides")
|
||||
return fallbackServerOverridesConfig{}
|
||||
}
|
||||
if overrideCfg.OrbitChannel != "" {
|
||||
if err := c.Set("orbit-channel", overrideCfg.OrbitChannel); err != nil {
|
||||
log.Error().Err(err).Str("component", "orbit").Msg("failed to set server overrides")
|
||||
}
|
||||
}
|
||||
if overrideCfg.OsquerydChannel != "" {
|
||||
if err := c.Set("osqueryd-channel", overrideCfg.OsquerydChannel); err != nil {
|
||||
log.Error().Err(err).Str("component", "osqueryd").Msg("failed to set server overrides")
|
||||
}
|
||||
}
|
||||
if overrideCfg.DesktopChannel != "" {
|
||||
if err := c.Set("desktop-channel", overrideCfg.DesktopChannel); err != nil {
|
||||
log.Error().Err(err).Str("component", "desktop").Msg("failed to set server overrides")
|
||||
}
|
||||
}
|
||||
|
||||
return overrideCfg.fallbackServerOverridesConfig
|
||||
}
|
||||
|
||||
// getFleetdComponentPaths returns the paths of the fleetd components.
|
||||
// If the path to the component cannot be fetched using the updater (e.g. channel doesn't exist yet)
|
||||
// then it will use the fallbackCfg's paths (if set).
|
||||
func getFleetdComponentPaths(
|
||||
c *cli.Context,
|
||||
updater *update.Updater,
|
||||
fallbackCfg fallbackServerOverridesConfig,
|
||||
) (osquerydPath string, desktopPath string, err error) {
|
||||
if err := updater.UpdateMetadata(); err != nil {
|
||||
log.Error().Err(err).Msg("update metadata before getting components")
|
||||
}
|
||||
|
||||
// osqueryd
|
||||
osquerydLocalTarget, err := updater.Get("osqueryd")
|
||||
if err != nil {
|
||||
if fallbackCfg.OsquerydPath == "" {
|
||||
log.Info().Err(err).Msg("get osqueryd target failed")
|
||||
return "", "", fmt.Errorf("get osqueryd target: %w", err)
|
||||
}
|
||||
log.Info().Err(err).Msgf("get osqueryd target failed, fallback to using %s", fallbackCfg.OsquerydPath)
|
||||
osquerydPath = fallbackCfg.OsquerydPath
|
||||
} else {
|
||||
osquerydPath = osquerydLocalTarget.ExecPath
|
||||
}
|
||||
|
||||
// Fleet Desktop
|
||||
if c.Bool("fleet-desktop") {
|
||||
fleetDesktopLocalTarget, err := updater.Get("desktop")
|
||||
if err != nil {
|
||||
if fallbackCfg.DesktopPath == "" {
|
||||
log.Info().Err(err).Msg("get desktop target failed")
|
||||
return "", "", fmt.Errorf("get desktop target: %w", err)
|
||||
}
|
||||
log.Info().Err(err).Msgf("get desktop target failed, fallback to using %s", fallbackCfg.DesktopPath)
|
||||
desktopPath = fallbackCfg.DesktopPath
|
||||
} else {
|
||||
if runtime.GOOS == "darwin" {
|
||||
desktopPath = fleetDesktopLocalTarget.DirPath
|
||||
} else {
|
||||
desktopPath = fleetDesktopLocalTarget.ExecPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return osquerydPath, desktopPath, nil
|
||||
}
|
||||
|
||||
func registerExtensionRunner(g *run.Group, extSockPath string, opts ...table.Opt) {
|
||||
ext := table.NewRunner(extSockPath, opts...)
|
||||
g.Add(ext.Execute, ext.Interrupt)
|
||||
@ -1378,3 +1471,190 @@ func writeSecret(enrollSecret string, orbitRoot string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// serverOverridesRunner is a oklog.Group runner that polls for configuration overrides from Fleet.
|
||||
type serverOverridesRunner struct {
|
||||
configFetcher update.OrbitConfigFetcher
|
||||
interval time.Duration
|
||||
rootDir string
|
||||
fallbackCfg fallbackServerOverridesConfig
|
||||
desktopEnabled bool
|
||||
cancel chan struct{}
|
||||
}
|
||||
|
||||
// newServerOverridesRunner creates a runner for updating server overrides configuration with values fetched from Fleet.
|
||||
func newServerOverridesRunner(
|
||||
configFetcher update.OrbitConfigFetcher,
|
||||
rootDir string,
|
||||
interval time.Duration,
|
||||
fallbackCfg fallbackServerOverridesConfig,
|
||||
desktopEnabled bool,
|
||||
) *serverOverridesRunner {
|
||||
return &serverOverridesRunner{
|
||||
configFetcher: configFetcher,
|
||||
interval: interval,
|
||||
rootDir: rootDir,
|
||||
fallbackCfg: fallbackCfg,
|
||||
desktopEnabled: desktopEnabled,
|
||||
cancel: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute starts the loop that polls for server overrides configuration from Fleet.
|
||||
func (r *serverOverridesRunner) Execute() error {
|
||||
log.Debug().Msg("starting server overrides runner")
|
||||
|
||||
ticker := time.NewTicker(r.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.cancel:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
log.Debug().Msg("calling server overrides run")
|
||||
didUpdate, err := r.run()
|
||||
if err != nil {
|
||||
logging.LogErrIfEnvNotSet(constant.SilenceEnrollLogErrorEnvVar, err, "server overrides run failed")
|
||||
}
|
||||
if didUpdate {
|
||||
log.Info().Msg("server overrides updated, exiting")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Interrupt is the oklog/run interrupt method that stops orbit when interrupt is received
|
||||
func (r *serverOverridesRunner) Interrupt(err error) {
|
||||
close(r.cancel)
|
||||
log.Error().Err(err).Msg("interrupt for server overrides runner")
|
||||
}
|
||||
|
||||
func (r *serverOverridesRunner) run() (bool, error) {
|
||||
overrideCfg, err := loadServerOverrides(r.rootDir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
orbitCfg, err := r.configFetcher.GetConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if orbitCfg.UpdateChannels == nil {
|
||||
// Server is not setting or doesn't know of
|
||||
// this feature (old server version), so nothing to do.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if cfgsDiffer(overrideCfg, orbitCfg, r.desktopEnabled) {
|
||||
if err := r.updateServerOverrides(orbitCfg); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// cfgsDiffer returns whether the local server overrides differ from the fetched remotely.
|
||||
func cfgsDiffer(overrideCfg *serverOverridesConfig, orbitCfg *fleet.OrbitConfig, desktopEnabled bool) bool {
|
||||
localUpdateChannelsCfg := &fleet.OrbitUpdateChannels{
|
||||
Orbit: overrideCfg.OrbitChannel,
|
||||
Osqueryd: overrideCfg.OsquerydChannel,
|
||||
Desktop: overrideCfg.DesktopChannel,
|
||||
}
|
||||
remoteUpdateChannelsCfg := orbitCfg.UpdateChannels
|
||||
|
||||
setStableAsDefault := func(cfg *fleet.OrbitUpdateChannels) {
|
||||
if cfg.Orbit == "" {
|
||||
cfg.Orbit = "stable"
|
||||
}
|
||||
if cfg.Osqueryd == "" {
|
||||
cfg.Osqueryd = "stable"
|
||||
}
|
||||
if cfg.Desktop == "" {
|
||||
cfg.Desktop = "stable"
|
||||
}
|
||||
}
|
||||
setStableAsDefault(localUpdateChannelsCfg)
|
||||
setStableAsDefault(remoteUpdateChannelsCfg)
|
||||
|
||||
local := *localUpdateChannelsCfg
|
||||
remote := *remoteUpdateChannelsCfg
|
||||
|
||||
if !desktopEnabled {
|
||||
local.Desktop = ""
|
||||
remote.Desktop = ""
|
||||
}
|
||||
|
||||
return local != remote
|
||||
}
|
||||
|
||||
// serverOverridesConfig holds the currently supported fields that can be
|
||||
// overriden by server configuration.
|
||||
type serverOverridesConfig struct {
|
||||
// OrbitChannel defines the override for the orbit's channel.
|
||||
OrbitChannel string `json:"orbit-channel"`
|
||||
// OsquerydChannel defines the override for the osqueryd's channel.
|
||||
OsquerydChannel string `json:"osqueryd-channel"`
|
||||
// DesktopChannel defines the override for the Fleet Desktop's channel.
|
||||
DesktopChannel string `json:"desktop-channel"`
|
||||
|
||||
fallbackServerOverridesConfig
|
||||
}
|
||||
|
||||
// fallbackServerOverridesConfig contains fallback configuration in case the server
|
||||
// settings are invalid (e.g. invalid update channels that don't exist).
|
||||
// Whenever the user sets an invalid channel, then the fallback paths are used to get
|
||||
// fleetd up and running with the last known good configuration.
|
||||
//
|
||||
// NOTE: We don't need orbit's path because the `orbit` component is a special case that uses
|
||||
// a symlink to define the last known valid version.
|
||||
type fallbackServerOverridesConfig struct {
|
||||
// OsquerydPath contains the path of the osqueryd executable last known to be valid.
|
||||
OsquerydPath string `json:"fallback-osqueryd-path"`
|
||||
// DesktopPath contains the path of the Fleet Desktop executable last known to be valid.
|
||||
DesktopPath string `json:"fallback-desktop-path"`
|
||||
}
|
||||
|
||||
func (f fallbackServerOverridesConfig) empty() bool {
|
||||
return f.OsquerydPath == "" && f.DesktopPath == ""
|
||||
}
|
||||
|
||||
// updateServerOverrides updates the server override local file with the configuration fetched from Fleet.
|
||||
func (r *serverOverridesRunner) updateServerOverrides(remoteCfg *fleet.OrbitConfig) error {
|
||||
overrideCfg := serverOverridesConfig{
|
||||
OrbitChannel: remoteCfg.UpdateChannels.Orbit,
|
||||
OsquerydChannel: remoteCfg.UpdateChannels.Osqueryd,
|
||||
DesktopChannel: remoteCfg.UpdateChannels.Desktop,
|
||||
fallbackServerOverridesConfig: r.fallbackCfg,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(overrideCfg, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal override config: %w", err)
|
||||
}
|
||||
serverOverridesPath := filepath.Join(r.rootDir, constant.ServerOverridesFileName)
|
||||
if err := os.WriteFile(serverOverridesPath, data, constant.DefaultFileMode); err != nil {
|
||||
return fmt.Errorf("write override config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadServerOverrides loads the server overrides from the local file.
|
||||
func loadServerOverrides(rootDir string) (*serverOverridesConfig, error) {
|
||||
serverOverridesPath := filepath.Join(rootDir, constant.ServerOverridesFileName)
|
||||
data, err := os.ReadFile(serverOverridesPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return &serverOverridesConfig{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var cfg serverOverridesConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
133
orbit/cmd/orbit/orbit_test.go
Normal file
133
orbit/cmd/orbit/orbit_test.go
Normal file
@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCfgsDiffer(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
overrideCfg *serverOverridesConfig
|
||||
orbitConfig *fleet.OrbitConfig
|
||||
desktopEnabled bool
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "initial set of remote configuration",
|
||||
overrideCfg: &serverOverridesConfig{},
|
||||
orbitConfig: &fleet.OrbitConfig{
|
||||
UpdateChannels: &fleet.OrbitUpdateChannels{
|
||||
Orbit: "stable",
|
||||
Osqueryd: "stable",
|
||||
Desktop: "stable",
|
||||
},
|
||||
},
|
||||
desktopEnabled: false,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "initial set of remote configuration, omit some channels",
|
||||
overrideCfg: &serverOverridesConfig{},
|
||||
orbitConfig: &fleet.OrbitConfig{
|
||||
UpdateChannels: &fleet.OrbitUpdateChannels{
|
||||
Orbit: "stable",
|
||||
},
|
||||
},
|
||||
desktopEnabled: false,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "initial set of remote configuration, change orbit and omit some channels",
|
||||
overrideCfg: &serverOverridesConfig{},
|
||||
orbitConfig: &fleet.OrbitConfig{
|
||||
UpdateChannels: &fleet.OrbitUpdateChannels{
|
||||
Orbit: "edge",
|
||||
},
|
||||
},
|
||||
desktopEnabled: false,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "initial set of remote configuration, set desktop when Fleet Desktop disabled",
|
||||
overrideCfg: &serverOverridesConfig{},
|
||||
orbitConfig: &fleet.OrbitConfig{
|
||||
UpdateChannels: &fleet.OrbitUpdateChannels{
|
||||
Desktop: "foobar",
|
||||
},
|
||||
},
|
||||
desktopEnabled: false,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "initial set of remote configuration, set desktop with Fleet Desktop enabled",
|
||||
overrideCfg: &serverOverridesConfig{},
|
||||
orbitConfig: &fleet.OrbitConfig{
|
||||
UpdateChannels: &fleet.OrbitUpdateChannels{
|
||||
Desktop: "foobar",
|
||||
},
|
||||
},
|
||||
desktopEnabled: true,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "overrides update, set desktop with Fleet Desktop enabled",
|
||||
overrideCfg: &serverOverridesConfig{
|
||||
DesktopChannel: "other",
|
||||
},
|
||||
orbitConfig: &fleet.OrbitConfig{
|
||||
UpdateChannels: &fleet.OrbitUpdateChannels{
|
||||
Desktop: "foobar",
|
||||
},
|
||||
},
|
||||
desktopEnabled: true,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "overrides update, change orbit",
|
||||
overrideCfg: &serverOverridesConfig{
|
||||
OrbitChannel: "first",
|
||||
},
|
||||
orbitConfig: &fleet.OrbitConfig{
|
||||
UpdateChannels: &fleet.OrbitUpdateChannels{
|
||||
Orbit: "second",
|
||||
},
|
||||
},
|
||||
desktopEnabled: false,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "overrides update, change osqueryd",
|
||||
overrideCfg: &serverOverridesConfig{
|
||||
OsquerydChannel: "first",
|
||||
},
|
||||
orbitConfig: &fleet.OrbitConfig{
|
||||
UpdateChannels: &fleet.OrbitUpdateChannels{
|
||||
Osqueryd: "second",
|
||||
},
|
||||
},
|
||||
desktopEnabled: false,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "overrides update, empty means stable",
|
||||
overrideCfg: &serverOverridesConfig{
|
||||
OrbitChannel: "stable",
|
||||
OsquerydChannel: "stable",
|
||||
DesktopChannel: "stable",
|
||||
},
|
||||
orbitConfig: &fleet.OrbitConfig{
|
||||
UpdateChannels: &fleet.OrbitUpdateChannels{},
|
||||
},
|
||||
desktopEnabled: true,
|
||||
expected: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
v := cfgsDiffer(tc.overrideCfg, tc.orbitConfig, tc.desktopEnabled)
|
||||
require.Equal(t, tc.expected, v)
|
||||
})
|
||||
}
|
||||
}
|
@ -49,4 +49,7 @@ const (
|
||||
UpdateTLSClientKeyFileName = "update_client.key"
|
||||
// SilenceEnrollLogErrorEnvVer is an environment variable name for disabling enroll log errors
|
||||
SilenceEnrollLogErrorEnvVar = "FLEETD_SILENCE_ENROLL_ERROR"
|
||||
// ServerOverridesFileName is the name of the file in the root directory
|
||||
// that specifies the override configuration fetched from the server.
|
||||
ServerOverridesFileName = "server-overrides.json"
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ package orbit_info
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/build"
|
||||
orbit_table "github.com/fleetdm/fleet/v4/orbit/pkg/table"
|
||||
@ -13,6 +14,7 @@ import (
|
||||
|
||||
// Extension implements an extension table that provides info about Orbit.
|
||||
type Extension struct {
|
||||
startTime time.Time
|
||||
orbitClient *service.OrbitClient
|
||||
orbitChannel string
|
||||
osquerydChannel string
|
||||
@ -22,8 +24,9 @@ type Extension struct {
|
||||
|
||||
var _ orbit_table.Extension = (*Extension)(nil)
|
||||
|
||||
func New(orbitClient *service.OrbitClient, orbitChannel, osquerydChannel, desktopChannel string, trw *token.ReadWriter) *Extension {
|
||||
func New(orbitClient *service.OrbitClient, orbitChannel, osquerydChannel, desktopChannel string, trw *token.ReadWriter, startTime time.Time) *Extension {
|
||||
return &Extension{
|
||||
startTime: startTime,
|
||||
orbitClient: orbitClient,
|
||||
orbitChannel: orbitChannel,
|
||||
osquerydChannel: osquerydChannel,
|
||||
@ -47,6 +50,7 @@ func (o Extension) Columns() []table.ColumnDefinition {
|
||||
table.TextColumn("orbit_channel"),
|
||||
table.TextColumn("osqueryd_channel"),
|
||||
table.TextColumn("desktop_channel"),
|
||||
table.BigIntColumn("uptime"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,5 +81,6 @@ func (o Extension) GenerateFunc(_ context.Context, _ table.QueryContext) ([]map[
|
||||
"orbit_channel": o.orbitChannel,
|
||||
"osqueryd_channel": o.osquerydChannel,
|
||||
"desktop_channel": o.desktopChannel,
|
||||
"uptime": strconv.FormatInt(int64(time.Since(o.startTime).Seconds()), 10),
|
||||
}}, nil
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/theupdateframework/go-tuf/client"
|
||||
)
|
||||
|
||||
// RunnerOptions is options provided for the update runner.
|
||||
@ -103,6 +104,12 @@ func NewRunner(updater *Updater, opt RunnerOptions) (*Runner, error) {
|
||||
// (knowing that they are not expected to change during the execution of the runner).
|
||||
for _, target := range opt.Targets {
|
||||
if err := runner.StoreLocalHash(target); err != nil {
|
||||
var tufFileNotFoundErr client.ErrNotFound
|
||||
if errors.As(err, &tufFileNotFoundErr) {
|
||||
// This can happen if the remote channel doesn't exist for a target.
|
||||
// We don't want to error out, so we skip such target.
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@ -234,7 +241,6 @@ func (r *Runner) UpdateAction() (bool, error) {
|
||||
// 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 {
|
||||
return didUpdate, fmt.Errorf("update %s: %w", target, err)
|
||||
|
@ -18,6 +18,8 @@ type AgentOptions struct {
|
||||
CommandLineStartUpFlags json.RawMessage `json:"command_line_flags,omitempty"`
|
||||
// Extensions are the orbit managed extensions
|
||||
Extensions json.RawMessage `json:"extensions,omitempty"`
|
||||
// UpdateChannels holds the configured channels for fleetd components.
|
||||
UpdateChannels json.RawMessage `json:"update_channels,omitempty"`
|
||||
}
|
||||
|
||||
type AgentOptionsOverrides struct {
|
||||
@ -61,6 +63,23 @@ func ValidateJSONAgentOptions(ctx context.Context, ds Datastore, rawJSON json.Ra
|
||||
}
|
||||
}
|
||||
|
||||
if len(opts.UpdateChannels) > 0 {
|
||||
if !isPremium {
|
||||
// The update_channels feature is premium only.
|
||||
return ErrMissingLicense
|
||||
}
|
||||
if string(opts.UpdateChannels) == "null" {
|
||||
return errors.New("update_channels cannot be null")
|
||||
}
|
||||
if err := checkEmptyFields("update_channels", opts.UpdateChannels); err != nil {
|
||||
return err
|
||||
}
|
||||
var updateChannels OrbitUpdateChannels
|
||||
if err := JSONStrictDecode(bytes.NewReader(opts.UpdateChannels), &updateChannels); err != nil {
|
||||
return fmt.Errorf("update_channels: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(opts.Config) > 0 {
|
||||
if err := validateJSONAgentOptionsSet(opts.Config); err != nil {
|
||||
return fmt.Errorf("common config: %w", err)
|
||||
@ -88,6 +107,22 @@ func ValidateJSONAgentOptions(ctx context.Context, ds Datastore, rawJSON json.Ra
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEmptyFields(prefix string, data json.RawMessage) error {
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return fmt.Errorf("unmarshal data: %w", err)
|
||||
}
|
||||
for k, v := range m {
|
||||
if v == nil {
|
||||
return fmt.Errorf("%s.%s is defined but not set", prefix, k)
|
||||
}
|
||||
if s, ok := v.(string); ok && s == "" {
|
||||
return fmt.Errorf("%s.%s is set to an empty string", prefix, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateJSONAgentOptionsExtensions(ctx context.Context, ds Datastore, optsExtensions json.RawMessage, isPremium bool) error {
|
||||
var extensions map[string]ExtensionInfo
|
||||
if err := json.Unmarshal(optsExtensions, &extensions); err != nil {
|
||||
|
@ -10,37 +10,38 @@ import (
|
||||
|
||||
func TestValidateAgentOptions(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
in string
|
||||
wantErr string
|
||||
desc string
|
||||
in string
|
||||
isPremium bool
|
||||
wantErr string
|
||||
}{
|
||||
{"empty object", "{}", ""},
|
||||
{"empty config", `{"config":{}}`, ""},
|
||||
{"empty overrides", `{"overrides":{}}`, ""},
|
||||
{"empty object", "{}", true, ""},
|
||||
{"empty config", `{"config":{}}`, true, ""},
|
||||
{"empty overrides", `{"overrides":{}}`, true, ""},
|
||||
|
||||
{"unknown top-level key", `{"foo":1}`, `unknown field "foo"`},
|
||||
{"unknown config key", `{"config":{"foo":1}}`, `unknown field "foo"`},
|
||||
{"unknown overrides key", `{"overrides": {"foo": 1}}`, `unknown field "foo"`},
|
||||
{"unknown top-level key", `{"foo":1}`, true, `unknown field "foo"`},
|
||||
{"unknown config key", `{"config":{"foo":1}}`, true, `unknown field "foo"`},
|
||||
{"unknown overrides key", `{"overrides": {"foo": 1}}`, true, `unknown field "foo"`},
|
||||
{"unknown overrides config key", `{"overrides": {
|
||||
"platforms": {
|
||||
"linux": {"foo":1}
|
||||
}
|
||||
}}`, `unknown field "foo"`},
|
||||
}}`, true, `unknown field "foo"`},
|
||||
|
||||
{"overrides.platform is null", `{"overrides": {
|
||||
"platforms": {
|
||||
"darwin": null
|
||||
}
|
||||
}}`, `platforms cannot be null. To remove platform overrides omit overrides from agent options.`},
|
||||
}}`, true, `platforms cannot be null. To remove platform overrides omit overrides from agent options.`},
|
||||
|
||||
{"extra top-level bytes", `{}true`, `extra bytes`},
|
||||
{"extra config bytes", `{"config":{}true}`, `invalid character 't' after object`},
|
||||
{"extra overrides bytes", `{"overrides":{}true}`, `invalid character 't' after object`},
|
||||
{"extra top-level bytes", `{}true`, true, `extra bytes`},
|
||||
{"extra config bytes", `{"config":{}true}`, true, `invalid character 't' after object`},
|
||||
{"extra overrides bytes", `{"overrides":{}true}`, true, `invalid character 't' after object`},
|
||||
|
||||
{"valid config", `{"config":{
|
||||
"options": {"aws_debug": true, "events_max": 3},
|
||||
"views": {"view1": "select 1"}
|
||||
}}`, ""},
|
||||
}}`, true, ""},
|
||||
{"valid overrides", `{"overrides":{
|
||||
"platforms": {
|
||||
"linux": {
|
||||
@ -52,27 +53,27 @@ func TestValidateAgentOptions(t *testing.T) {
|
||||
"views": {"view2": "select 2"}
|
||||
}
|
||||
}
|
||||
}}`, ""},
|
||||
}}`, true, ""},
|
||||
|
||||
{"invalid config value", `{"config":{
|
||||
"events": {
|
||||
"disable_subscribers": true
|
||||
},
|
||||
"options": {"aws_debug": 1}
|
||||
}}`, "cannot unmarshal bool into Go struct field .events.disable_subscribers of type []string"},
|
||||
}}`, true, "cannot unmarshal bool into Go struct field .events.disable_subscribers of type []string"},
|
||||
{"invalid overrides value", `{"overrides":{
|
||||
"platforms": {
|
||||
"linux": {
|
||||
"options": {"aws_debug": true, "events_max": "nope"}
|
||||
}
|
||||
}
|
||||
}}`, `cannot unmarshal string into Go struct field osqueryOptions.options.events_max of type uint64`},
|
||||
}}`, true, `cannot unmarshal string into Go struct field osqueryOptions.options.events_max of type uint64`},
|
||||
|
||||
{"valid packs string", `{"config":{
|
||||
"packs": {
|
||||
"pack1": "ok"
|
||||
}
|
||||
}}`, ""},
|
||||
}}`, true, ""},
|
||||
{"valid packs object", `{"config":{
|
||||
"packs": {
|
||||
"pack1": {
|
||||
@ -84,7 +85,7 @@ func TestValidateAgentOptions(t *testing.T) {
|
||||
"platform": "darwin"
|
||||
}
|
||||
}
|
||||
}}`, ""},
|
||||
}}`, true, ""},
|
||||
{"invalid packs object key is accepted as we do not validate packs", `{"config":{
|
||||
"packs": {
|
||||
"pack1": {
|
||||
@ -97,57 +98,90 @@ func TestValidateAgentOptions(t *testing.T) {
|
||||
"platform": "darwin"
|
||||
}
|
||||
}
|
||||
}}`, ``},
|
||||
}}`, true, ``},
|
||||
{"invalid packs type is accepted as we do not validate packs", `{"config":{
|
||||
"packs": {
|
||||
"pack1": 1
|
||||
}
|
||||
}}`, ``},
|
||||
}}`, true, ``},
|
||||
{"invalid schedule type is accepted as we do not validate schedule", `{"config":{
|
||||
"schedule": {
|
||||
"foo": 1
|
||||
}
|
||||
}}`, ``},
|
||||
}}`, true, ``},
|
||||
{"option added in osquery 5.5.1", `{"config":{
|
||||
"options": {
|
||||
"malloc_trim_threshold": 100
|
||||
}
|
||||
}}`, ``},
|
||||
}}`, true, ``},
|
||||
{"option removed in osquery 5.5.1", `{"config":{
|
||||
"options": {
|
||||
"yara_malloc_trim": true
|
||||
}
|
||||
}}`, `unknown field "yara_malloc_trim"`},
|
||||
}}`, true, `unknown field "yara_malloc_trim"`},
|
||||
{"valid command-line flag", `{"command_line_flags":{
|
||||
"alarm_timeout": 1
|
||||
}}`, ``},
|
||||
}}`, true, ``},
|
||||
{"invalid command-line flag", `{"command_line_flags":{
|
||||
"no_such_flag": true
|
||||
}}`, `unknown field "no_such_flag"`},
|
||||
}}`, true, `unknown field "no_such_flag"`},
|
||||
{"invalid command-line value", `{"command_line_flags":{
|
||||
"enable_tables": 123
|
||||
}}`, `cannot unmarshal number into Go struct field osqueryCommandLineFlags.enable_tables of type string`},
|
||||
}}`, true, `cannot unmarshal number into Go struct field osqueryCommandLineFlags.enable_tables of type string`},
|
||||
{"setting a valid os-specific flag", `{"command_line_flags":{
|
||||
"users_service_delay": 123
|
||||
}}`, ``},
|
||||
}}`, true, ``},
|
||||
{"setting a valid os-specific option", `{"config":{
|
||||
"options": {
|
||||
"users_service_delay": 123
|
||||
}
|
||||
}}`, ``},
|
||||
}}`, true, ``},
|
||||
{"setting an invalid value for an os-specific flag", `{"command_line_flags":{
|
||||
"disable_endpointsecurity": "ok"
|
||||
}}`, `command-line flags: json: cannot unmarshal string into Go struct field osqueryCommandLineFlags.disable_endpointsecurity of type bool`},
|
||||
}}`, true, `command-line flags: json: cannot unmarshal string into Go struct field osqueryCommandLineFlags.disable_endpointsecurity of type bool`},
|
||||
{"setting an invalid value for an os-specific option", `{"config":{
|
||||
"options": {
|
||||
"disable_endpointsecurity": "ok"
|
||||
}
|
||||
}}`, `common config: json: cannot unmarshal string into Go struct field osqueryOptions.options.disable_endpointsecurity of type bool`},
|
||||
}}`, true, `common config: json: cannot unmarshal string into Go struct field osqueryOptions.options.disable_endpointsecurity of type bool`},
|
||||
{"setting an empty update_channels", `{
|
||||
"update_channels": null
|
||||
}`, true, `update_channels cannot be null`},
|
||||
{"setting a empty channel in update_channels", `{
|
||||
"update_channels": {
|
||||
"osqueryd": "5.10.2",
|
||||
"orbit": null
|
||||
}
|
||||
}`, true, `update_channels.orbit is defined but not set`},
|
||||
{"setting a channel in update_channels to empty string", `{
|
||||
"update_channels": {
|
||||
"osqueryd": "5.10.2",
|
||||
"orbit": ""
|
||||
}
|
||||
}`, true, `update_channels.orbit is set to an empty string`},
|
||||
{"setting a channel to unknown component in update_channels", `{
|
||||
"update_channels": {
|
||||
"osqueryd": "5.10.2",
|
||||
"unknown": "foobar"
|
||||
}
|
||||
}`, true, `update_channels: json: unknown field "unknown"`},
|
||||
{"setting update_channels non-premium", `{
|
||||
"update_channels": {
|
||||
"osqueryd": "5.10.2",
|
||||
"orbit": "foobar"
|
||||
}
|
||||
}`, false, `Requires Fleet Premium license`},
|
||||
{"setting update_channels", `{
|
||||
"update_channels": {
|
||||
"osqueryd": "5.10.2",
|
||||
"orbit": "foobar"
|
||||
}
|
||||
}`, true, ``},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
err := ValidateJSONAgentOptions(context.Background(), nil, []byte(c.in), true)
|
||||
err := ValidateJSONAgentOptions(context.Background(), nil, []byte(c.in), c.isPremium)
|
||||
t.Logf("%T", errors.Unwrap(err))
|
||||
if c.wantErr != "" {
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
|
@ -40,6 +40,20 @@ type OrbitConfig struct {
|
||||
Extensions json.RawMessage `json:"extensions,omitempty"`
|
||||
NudgeConfig *NudgeConfig `json:"nudge_config,omitempty"`
|
||||
Notifications OrbitConfigNotifications `json:"notifications,omitempty"`
|
||||
// UpdateChannels contains the TUF channels to use on fleetd components.
|
||||
//
|
||||
// If UpdateChannels is nil it means the server isn't using/setting this feature.
|
||||
UpdateChannels *OrbitUpdateChannels `json:"update_channels,omitempty"`
|
||||
}
|
||||
|
||||
// OrbitUpdateChannels hold the update channels that can be configured in fleetd agents.
|
||||
type OrbitUpdateChannels struct {
|
||||
// Orbit holds the orbit channel.
|
||||
Orbit string `json:"orbit"`
|
||||
// Osqueryd holds the osqueryd channel.
|
||||
Osqueryd string `json:"osqueryd"`
|
||||
// Desktop holds the Fleet Desktop channel.
|
||||
Desktop string `json:"desktop"`
|
||||
}
|
||||
|
||||
// OrbitHostInfo holds device information used during Orbit enroll.
|
||||
|
@ -280,11 +280,21 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
|
||||
notifs.EnforceBitLockerEncryption = true
|
||||
}
|
||||
|
||||
var updateChannels *fleet.OrbitUpdateChannels
|
||||
if len(opts.UpdateChannels) > 0 {
|
||||
var uc fleet.OrbitUpdateChannels
|
||||
if err := json.Unmarshal(opts.UpdateChannels, &uc); err != nil {
|
||||
return fleet.OrbitConfig{}, err
|
||||
}
|
||||
updateChannels = &uc
|
||||
}
|
||||
|
||||
return fleet.OrbitConfig{
|
||||
Flags: opts.CommandLineStartUpFlags,
|
||||
Extensions: extensionsFiltered,
|
||||
Notifications: notifs,
|
||||
NudgeConfig: nudgeConfig,
|
||||
Flags: opts.CommandLineStartUpFlags,
|
||||
Extensions: extensionsFiltered,
|
||||
Notifications: notifs,
|
||||
NudgeConfig: nudgeConfig,
|
||||
UpdateChannels: updateChannels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -316,11 +326,21 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
|
||||
notifs.EnforceBitLockerEncryption = true
|
||||
}
|
||||
|
||||
var updateChannels *fleet.OrbitUpdateChannels
|
||||
if len(opts.UpdateChannels) > 0 {
|
||||
var uc fleet.OrbitUpdateChannels
|
||||
if err := json.Unmarshal(opts.UpdateChannels, &uc); err != nil {
|
||||
return fleet.OrbitConfig{}, err
|
||||
}
|
||||
updateChannels = &uc
|
||||
}
|
||||
|
||||
return fleet.OrbitConfig{
|
||||
Flags: opts.CommandLineStartUpFlags,
|
||||
Extensions: extensionsFiltered,
|
||||
Notifications: notifs,
|
||||
NudgeConfig: nudgeConfig,
|
||||
Flags: opts.CommandLineStartUpFlags,
|
||||
Extensions: extensionsFiltered,
|
||||
Notifications: notifs,
|
||||
NudgeConfig: nudgeConfig,
|
||||
UpdateChannels: updateChannels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user