Files
fleet/orbit/pkg/update/flag_runner_test.go
Lucas Manuel Rodriguez 4991b3c685 Fix fleetd with pre-packaged osquery flags (#47305)
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 -->
2026-06-10 17:13:26 -03:00

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))
}