Resolves #47285. Final behavior (matches pre-1.56): ``` ┌────────────────────┬─────────────────────────┬───────────────────────────────────────┐ │ command_line_flags │ wire config.Flags │ Orbit action │ ├────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Unset │ omitted (nil, len == 0) │ Preserve osquery.flags — return early │ ├────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ null │ null (len == 4) │ Clear (parses to empty map) │ ├────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ {} │ {} (len == 2) │ Clear (parses to empty map) │ ├────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ {"verbose": true} │ non-empty │ Write flags │ └────────────────────┴─────────────────────────┴───────────────────────────────────────┘ ``` - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [X] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [x] Verified that fleetd runs on macOS, Linux and Windows - [x] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Preserve existing pre-packaged or user-provided osquery flagfiles when the server omits flag configuration (no unintended clears or restarts). * Treat an explicit empty flag payload (including JSON empty document or JSON "null") as an instruction to clear flagfiles and queue a restart. * **Tests** * Expanded coverage to validate the preserve-vs-clear behavior for unset, empty, and "null" flag payloads. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
190 lines
5.6 KiB
Go
190 lines
5.6 KiB
Go
package update
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var rawJSONFlags = json.RawMessage(`{"verbose":true, "num":5, "hello":"world", "largeNum":1234567890}`)
|
|
|
|
func TestGetFlagsFromJson(t *testing.T) {
|
|
flagsJson, err := getFlagsFromJSON(rawJSONFlags)
|
|
require.NoError(t, err)
|
|
|
|
require.NotEmpty(t, flagsJson)
|
|
|
|
value, ok := flagsJson["--verbose"]
|
|
if !ok {
|
|
t.Errorf(`key ""--verbose" expected but not found`)
|
|
}
|
|
if value != "true" {
|
|
t.Errorf(`expected "true", got %s`, value)
|
|
}
|
|
|
|
value, ok = flagsJson["--num"]
|
|
if !ok {
|
|
t.Errorf(`key "--num" expected but not found`)
|
|
}
|
|
if value != "5" {
|
|
t.Errorf(`expected "5", got %s`, value)
|
|
}
|
|
|
|
value, ok = flagsJson["--hello"]
|
|
if !ok {
|
|
t.Errorf(`key "--hello" expected but not found`)
|
|
}
|
|
if value != "world" {
|
|
t.Errorf(`expected "world", got %s`, value)
|
|
}
|
|
|
|
value, ok = flagsJson["--largeNum"]
|
|
if !ok {
|
|
t.Errorf(`key "--largeNum" expected but not found`)
|
|
}
|
|
if value != "1234567890" {
|
|
t.Errorf(`expected "1234567890", got %s`, value)
|
|
}
|
|
}
|
|
|
|
func TestWriteFlagFile(t *testing.T) {
|
|
flags, err := getFlagsFromJSON(rawJSONFlags)
|
|
require.NoError(t, err)
|
|
|
|
tempDir := t.TempDir()
|
|
err = writeFlagFile(tempDir, flags)
|
|
require.NoError(t, err)
|
|
|
|
diskFlags, err := readFlagFile(tempDir)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, diskFlags)
|
|
|
|
if !reflect.DeepEqual(flags, diskFlags) {
|
|
t.Errorf("expected flags to be equal: %v, %v", flags, diskFlags)
|
|
}
|
|
}
|
|
|
|
func touchFile(t *testing.T, name string) {
|
|
t.Helper()
|
|
|
|
file, err := os.OpenFile(name, os.O_RDONLY|os.O_CREATE, 0o644) // nolint:gosec // G302
|
|
require.NoError(t, err)
|
|
require.NoError(t, file.Close())
|
|
}
|
|
|
|
// TestDoFlagsUpdateWithEmptyFlags tests the scenario of Fleet flag `command_line_flags`
|
|
// being set to an empty JSON document `{}`. Setting it to `{}` is an explicit instruction
|
|
// to clear osquery flags, so Orbit reconciles the osquery.flags file to empty (distinct
|
|
// from command_line_flags being unset, which is covered by TestDoFlagsUpdateWithNilFlags).
|
|
func TestDoFlagsUpdateWithEmptyFlags(t *testing.T) {
|
|
rootDir := t.TempDir()
|
|
osqueryFlagsFile := filepath.Join(rootDir, "osquery.flags")
|
|
touchFile(t, osqueryFlagsFile)
|
|
|
|
testConfig := &fleet.OrbitConfig{
|
|
Flags: json.RawMessage("{}"),
|
|
}
|
|
|
|
var restartQueued bool
|
|
queueOrbitRestart := func(string) { restartQueued = true }
|
|
|
|
fr := NewFlagReceiver(queueOrbitRestart, FlagUpdateOptions{
|
|
RootDir: rootDir,
|
|
})
|
|
|
|
err := fr.Run(testConfig)
|
|
require.NoError(t, err)
|
|
require.False(t, restartQueued)
|
|
|
|
// Non-empty fleet flags and osquery.flags has empty flags.
|
|
testConfig = &fleet.OrbitConfig{
|
|
Flags: json.RawMessage(`{"verbose": true}`),
|
|
}
|
|
err = fr.Run(testConfig)
|
|
require.NoError(t, err)
|
|
require.True(t, restartQueued)
|
|
|
|
// Empty Fleet flags ({}) and osquery.flags has non-empty flags: the file is
|
|
// cleared and a restart is triggered.
|
|
restartQueued = false
|
|
testConfig = &fleet.OrbitConfig{
|
|
Flags: json.RawMessage("{}"),
|
|
}
|
|
err = os.WriteFile(osqueryFlagsFile, []byte("--verbose=true\n"), 0o644)
|
|
require.NoError(t, err)
|
|
err = fr.Run(testConfig)
|
|
require.NoError(t, err)
|
|
require.True(t, restartQueued)
|
|
|
|
contents, err := os.ReadFile(osqueryFlagsFile)
|
|
require.NoError(t, err)
|
|
require.Empty(t, string(contents))
|
|
}
|
|
|
|
// TestDoFlagsUpdateWithNilFlags verifies that when the server is not managing
|
|
// osquery command-line flags (command_line_flags unset, so config.Flags is
|
|
// nil/empty), Orbit leaves the osquery.flags file untouched, preserving any
|
|
// pre-packaged or user-provided flags.
|
|
func TestDoFlagsUpdateWithNilFlags(t *testing.T) {
|
|
rootDir := t.TempDir()
|
|
osqueryFlagsFile := filepath.Join(rootDir, "osquery.flags")
|
|
|
|
var restartQueued bool
|
|
queueOrbitRestart := func(string) { restartQueued = true }
|
|
fr := NewFlagReceiver(queueOrbitRestart, FlagUpdateOptions{RootDir: rootDir})
|
|
|
|
// Nil Flags + no file: no-op.
|
|
err := fr.Run(&fleet.OrbitConfig{Flags: nil})
|
|
require.NoError(t, err)
|
|
require.False(t, restartQueued)
|
|
|
|
// Nil Flags + existing user-provided file: file is preserved, no restart.
|
|
userFlags := "--verbose=true\n--tls_dump=true\n"
|
|
err = os.WriteFile(osqueryFlagsFile, []byte(userFlags), 0o644)
|
|
require.NoError(t, err)
|
|
err = fr.Run(&fleet.OrbitConfig{Flags: nil})
|
|
require.NoError(t, err)
|
|
require.False(t, restartQueued)
|
|
|
|
contents, err := os.ReadFile(osqueryFlagsFile)
|
|
require.NoError(t, err)
|
|
require.Equal(t, userFlags, string(contents))
|
|
|
|
// Empty (but non-nil) Flags is treated the same as nil: file preserved.
|
|
err = fr.Run(&fleet.OrbitConfig{Flags: json.RawMessage("")})
|
|
require.NoError(t, err)
|
|
require.False(t, restartQueued)
|
|
|
|
contents, err = os.ReadFile(osqueryFlagsFile)
|
|
require.NoError(t, err)
|
|
require.Equal(t, userFlags, string(contents))
|
|
}
|
|
|
|
// TestDoFlagsUpdateWithNullFlags verifies that the JSON literal "null" is an
|
|
// explicit instruction to clear osquery flags, distinct from command_line_flags
|
|
// being unset (which preserves the file).
|
|
func TestDoFlagsUpdateWithNullFlags(t *testing.T) {
|
|
rootDir := t.TempDir()
|
|
osqueryFlagsFile := filepath.Join(rootDir, "osquery.flags")
|
|
|
|
var restartQueued bool
|
|
queueOrbitRestart := func(string) { restartQueued = true }
|
|
fr := NewFlagReceiver(queueOrbitRestart, FlagUpdateOptions{RootDir: rootDir})
|
|
|
|
// "null" + existing flags: the file is cleared and a restart is triggered.
|
|
err := os.WriteFile(osqueryFlagsFile, []byte("--verbose=true\n"), 0o644)
|
|
require.NoError(t, err)
|
|
err = fr.Run(&fleet.OrbitConfig{Flags: json.RawMessage("null")})
|
|
require.NoError(t, err)
|
|
require.True(t, restartQueued)
|
|
|
|
contents, err := os.ReadFile(osqueryFlagsFile)
|
|
require.NoError(t, err)
|
|
require.Empty(t, string(contents))
|
|
}
|