mirror of
https://github.com/fleetdm/fleet
synced 2026-05-05 22:39:17 +00:00
For #19016 This changes all the places where we previously assumed that certs were hardcoded when the Fleet server started to query the database instead. The plan is to loadtest afterwards, but as a first preemptive measure, this adds a caching layer on top the mysql datastore. # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [x] Manual QA for all new/changed functionality
848 lines
26 KiB
Go
848 lines
26 KiB
Go
package cached_mysql
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type nilCloner struct{}
|
|
|
|
func (n *nilCloner) Clone() (fleet.Cloner, error) {
|
|
var nn *nilCloner
|
|
return nn, nil
|
|
}
|
|
|
|
func TestClone(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
src fleet.Cloner
|
|
want fleet.Cloner
|
|
}{
|
|
{
|
|
name: "appconfig",
|
|
src: &fleet.AppConfig{
|
|
ServerSettings: fleet.ServerSettings{
|
|
EnableAnalytics: true,
|
|
},
|
|
},
|
|
want: &fleet.AppConfig{
|
|
ServerSettings: fleet.ServerSettings{
|
|
EnableAnalytics: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "nil",
|
|
src: (*nilCloner)(nil),
|
|
want: (*nilCloner)(nil),
|
|
},
|
|
{
|
|
name: "appconfig with nested slice",
|
|
src: &fleet.AppConfig{
|
|
ServerSettings: fleet.ServerSettings{
|
|
DebugHostIDs: []uint{1, 2, 3},
|
|
},
|
|
},
|
|
want: &fleet.AppConfig{
|
|
ServerSettings: fleet.ServerSettings{
|
|
DebugHostIDs: []uint{1, 2, 3},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
clone, err := tc.src.Clone()
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.want, clone)
|
|
|
|
// ensure that writing to src does not alter the cloned value (i.e. that
|
|
// the nested fields are deeply cloned too).
|
|
switch src := tc.src.(type) {
|
|
case *fleet.AppConfig:
|
|
if len(src.ServerSettings.DebugHostIDs) > 0 {
|
|
src.ServerSettings.DebugHostIDs[0] = 999
|
|
require.NotEqual(t, src.ServerSettings.DebugHostIDs, clone.(*fleet.AppConfig).ServerSettings.DebugHostIDs)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCachedAppConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS)
|
|
|
|
var appConfigSet *fleet.AppConfig
|
|
mockedDS.NewAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) (*fleet.AppConfig, error) {
|
|
appConfigSet = info
|
|
return info, nil
|
|
}
|
|
mockedDS.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return appConfigSet, nil
|
|
}
|
|
mockedDS.SaveAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) error {
|
|
appConfigSet = info
|
|
return nil
|
|
}
|
|
_, err := ds.NewAppConfig(context.Background(), &fleet.AppConfig{
|
|
Features: fleet.Features{
|
|
AdditionalQueries: ptr.RawMessage(json.RawMessage(`"TestCachedAppConfig"`)),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("NewAppConfig", func(t *testing.T) {
|
|
data, err := ds.AppConfig(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
require.NotEmpty(t, data)
|
|
require.Equal(t, json.RawMessage(`"TestCachedAppConfig"`), *data.Features.AdditionalQueries)
|
|
})
|
|
|
|
t.Run("AppConfig", func(t *testing.T) {
|
|
require.False(t, mockedDS.AppConfigFuncInvoked)
|
|
ac, err := ds.AppConfig(context.Background())
|
|
require.NoError(t, err)
|
|
require.False(t, mockedDS.AppConfigFuncInvoked)
|
|
|
|
require.Equal(t, ptr.RawMessage(json.RawMessage(`"TestCachedAppConfig"`)), ac.Features.AdditionalQueries)
|
|
})
|
|
|
|
t.Run("SaveAppConfig", func(t *testing.T) {
|
|
require.NoError(t, ds.SaveAppConfig(context.Background(), &fleet.AppConfig{
|
|
Features: fleet.Features{
|
|
AdditionalQueries: ptr.RawMessage(json.RawMessage(`"NewSAVED"`)),
|
|
},
|
|
}))
|
|
|
|
require.True(t, mockedDS.SaveAppConfigFuncInvoked)
|
|
|
|
ac, err := ds.AppConfig(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ac.Features.AdditionalQueries)
|
|
require.Equal(t, json.RawMessage(`"NewSAVED"`), *ac.Features.AdditionalQueries)
|
|
})
|
|
|
|
t.Run("External SaveAppConfig gets caught", func(t *testing.T) {
|
|
mockedDS.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{
|
|
Features: fleet.Features{
|
|
AdditionalQueries: ptr.RawMessage(json.RawMessage(`"SavedSomewhereElse"`)),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
ac, err := ds.AppConfig(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ac.Features.AdditionalQueries)
|
|
require.Equal(t, json.RawMessage(`"SavedSomewhereElse"`), *ac.Features.AdditionalQueries)
|
|
})
|
|
}
|
|
|
|
func TestBypassAppConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS, WithAppConfigExpiration(time.Minute))
|
|
|
|
var appConfigSet *fleet.AppConfig
|
|
mockedDS.NewAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) (*fleet.AppConfig, error) {
|
|
appConfigSet = info
|
|
return info, nil
|
|
}
|
|
mockedDS.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return appConfigSet, nil
|
|
}
|
|
mockedDS.SaveAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) error {
|
|
appConfigSet = info
|
|
return nil
|
|
}
|
|
|
|
// calling NewAppConfig initializes the cache
|
|
_, err := ds.NewAppConfig(context.Background(), &fleet.AppConfig{
|
|
OrgInfo: fleet.OrgInfo{
|
|
OrgName: "A",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
|
|
// used the cached value
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "A", ac.OrgInfo.OrgName)
|
|
require.False(t, mockedDS.AppConfigFuncInvoked)
|
|
|
|
// update and save it, calls the DB
|
|
ac.OrgInfo.OrgName = "B"
|
|
err = ds.SaveAppConfig(ctx, ac)
|
|
require.NoError(t, err)
|
|
require.True(t, mockedDS.SaveAppConfigFuncInvoked)
|
|
mockedDS.SaveAppConfigFuncInvoked = false
|
|
|
|
// read it back, uses the cache
|
|
ac, err = ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "B", ac.OrgInfo.OrgName)
|
|
require.False(t, mockedDS.AppConfigFuncInvoked)
|
|
|
|
// simulate a database change from another process, not via the cached_mysql store
|
|
ac.OrgInfo.OrgName = "C"
|
|
err = mockedDS.SaveAppConfig(ctx, ac)
|
|
require.NoError(t, err)
|
|
|
|
// reading it via the store uses the old cached value
|
|
ac, err = ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "B", ac.OrgInfo.OrgName)
|
|
require.False(t, mockedDS.AppConfigFuncInvoked)
|
|
|
|
// force-bypassing the cache gets the updated value
|
|
ctx = ctxdb.BypassCachedMysql(ctx, true)
|
|
ac, err = ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "C", ac.OrgInfo.OrgName)
|
|
require.True(t, mockedDS.AppConfigFuncInvoked)
|
|
mockedDS.AppConfigFuncInvoked = false
|
|
|
|
// bypassing the cache to read AppConfig did update the cache, so if we don't
|
|
// bypass it anymore, it now gets the updated value from the cache
|
|
ctx = ctxdb.BypassCachedMysql(ctx, false)
|
|
ac, err = ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "C", ac.OrgInfo.OrgName)
|
|
require.False(t, mockedDS.AppConfigFuncInvoked)
|
|
}
|
|
|
|
func TestCachedPacksforHost(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS, WithPacksExpiration(100*time.Millisecond))
|
|
|
|
dbPacks := []*fleet.Pack{
|
|
{
|
|
ID: 1,
|
|
Name: "test-pack-1",
|
|
},
|
|
{
|
|
ID: 2,
|
|
Name: "test-pack-2",
|
|
},
|
|
}
|
|
called := 0
|
|
mockedDS.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) {
|
|
called++
|
|
return dbPacks, nil
|
|
}
|
|
|
|
// first call gets the result from the DB
|
|
packs, err := ds.ListPacksForHost(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, dbPacks, packs)
|
|
require.Same(t, dbPacks[0], packs[0])
|
|
require.Equal(t, 1, called)
|
|
|
|
// change "stored" dbPacks.
|
|
dbPacks = []*fleet.Pack{
|
|
{
|
|
ID: 1,
|
|
Name: "test-pack-1",
|
|
},
|
|
{
|
|
ID: 3,
|
|
Name: "test-pack-3",
|
|
},
|
|
}
|
|
|
|
// this call gets it from the cache
|
|
packs2, err := ds.ListPacksForHost(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, packs, packs2) // returns the old cached value
|
|
require.NotSame(t, packs[0], packs2[0]) // have been cloned
|
|
require.Equal(t, 1, called)
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// this call gets it from the DB again since the cache expired
|
|
packs3, err := ds.ListPacksForHost(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, dbPacks, packs3)
|
|
require.Same(t, dbPacks[0], packs3[0])
|
|
require.Equal(t, 2, called)
|
|
}
|
|
|
|
func TestCachedListScheduledQueriesInPack(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS, WithScheduledQueriesExpiration(100*time.Millisecond))
|
|
|
|
dbScheduledQueries := fleet.ScheduledQueryList{
|
|
{
|
|
ID: 1,
|
|
Name: "test-schedule-1",
|
|
},
|
|
{
|
|
ID: 2,
|
|
Name: "test-schedule-2",
|
|
},
|
|
}
|
|
called := 0
|
|
mockedDS.ListScheduledQueriesInPackFunc = func(ctx context.Context, packID uint) (fleet.ScheduledQueryList, error) {
|
|
called++
|
|
return dbScheduledQueries, nil
|
|
}
|
|
|
|
// this initial call gets the result from the DB
|
|
scheduledQueries, err := ds.ListScheduledQueriesInPack(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, dbScheduledQueries, scheduledQueries)
|
|
require.Same(t, dbScheduledQueries[0], scheduledQueries[0])
|
|
require.Equal(t, 1, called)
|
|
|
|
// change "stored" dbScheduledQueries.
|
|
dbScheduledQueries = fleet.ScheduledQueryList{
|
|
{
|
|
ID: 3,
|
|
Name: "test-schedule-3",
|
|
},
|
|
}
|
|
|
|
// this call gets it from the cache
|
|
scheduledQueries2, err := ds.ListScheduledQueriesInPack(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, scheduledQueries2, scheduledQueries)
|
|
require.NotSame(t, scheduledQueries[0], scheduledQueries2[0]) // has been cloned
|
|
require.Equal(t, 1, called)
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// this call gets it from the DB again, since the cache expired
|
|
scheduledQueries3, err := ds.ListScheduledQueriesInPack(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, dbScheduledQueries, scheduledQueries3)
|
|
require.Same(t, dbScheduledQueries[0], scheduledQueries3[0])
|
|
require.Equal(t, 2, called)
|
|
}
|
|
|
|
func TestCachedTeamAgentOptions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS, WithTeamAgentOptionsExpiration(100*time.Millisecond))
|
|
|
|
testOptions := json.RawMessage(`
|
|
{
|
|
"config": {
|
|
"options": {
|
|
"logger_plugin": "tls",
|
|
"pack_delimiter": "/",
|
|
"logger_tls_period": 10,
|
|
"distributed_plugin": "tls",
|
|
"disable_distributed": false,
|
|
"logger_tls_endpoint": "/api/osquery/log",
|
|
"distributed_interval": 10,
|
|
"distributed_tls_max_attempts": 3
|
|
},
|
|
"decorators": {
|
|
"load": [
|
|
"SELECT uuid AS host_uuid FROM system_info;",
|
|
"SELECT hostname AS hostname FROM system_info;"
|
|
]
|
|
}
|
|
},
|
|
"overrides": {}
|
|
}
|
|
`)
|
|
|
|
testTeam := &fleet.Team{
|
|
ID: 1,
|
|
CreatedAt: time.Now(),
|
|
Name: "test",
|
|
Config: fleet.TeamConfig{
|
|
AgentOptions: &testOptions,
|
|
},
|
|
}
|
|
|
|
deleted := false
|
|
mockedDS.TeamAgentOptionsFunc = func(ctx context.Context, teamID uint) (*json.RawMessage, error) {
|
|
if deleted {
|
|
return nil, errors.New("not found")
|
|
}
|
|
return &testOptions, nil
|
|
}
|
|
mockedDS.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return team, nil
|
|
}
|
|
mockedDS.DeleteTeamFunc = func(ctx context.Context, teamID uint) error {
|
|
deleted = true
|
|
return nil
|
|
}
|
|
|
|
// initial call reads from the DB
|
|
options, err := ds.TeamAgentOptions(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, string(testOptions), string(*options))
|
|
require.Same(t, &testOptions, options)
|
|
require.True(t, mockedDS.TeamAgentOptionsFuncInvoked)
|
|
mockedDS.TeamAgentOptionsFuncInvoked = false
|
|
|
|
// subsequent call reads from the cache
|
|
options, err = ds.TeamAgentOptions(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, string(testOptions), string(*options))
|
|
require.NotSame(t, &testOptions, options)
|
|
require.False(t, mockedDS.TeamAgentOptionsFuncInvoked)
|
|
|
|
// saving a team updates agent options in cache
|
|
updateOptions := json.RawMessage(`
|
|
{}
|
|
`)
|
|
updateTeam := &fleet.Team{
|
|
ID: testTeam.ID,
|
|
CreatedAt: testTeam.CreatedAt,
|
|
Name: testTeam.Name,
|
|
Config: fleet.TeamConfig{
|
|
AgentOptions: &updateOptions,
|
|
},
|
|
}
|
|
|
|
_, err = ds.SaveTeam(context.Background(), updateTeam)
|
|
require.NoError(t, err)
|
|
require.True(t, mockedDS.SaveTeamFuncInvoked)
|
|
mockedDS.SaveTeamFuncInvoked = false
|
|
|
|
// reading reads it from the cache with the updated data
|
|
options, err = ds.TeamAgentOptions(context.Background(), testTeam.ID)
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, string(updateOptions), string(*options))
|
|
require.NotSame(t, &updateOptions, options)
|
|
require.False(t, mockedDS.TeamAgentOptionsFuncInvoked)
|
|
|
|
// deleting a team removes the agent options from the cache
|
|
err = ds.DeleteTeam(context.Background(), testTeam.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, mockedDS.DeleteTeamFuncInvoked)
|
|
mockedDS.DeleteTeamFuncInvoked = false
|
|
|
|
// reading hits the DB as the cached item was removed
|
|
_, err = ds.TeamAgentOptions(context.Background(), testTeam.ID)
|
|
require.Error(t, err)
|
|
require.True(t, mockedDS.TeamAgentOptionsFuncInvoked)
|
|
}
|
|
|
|
func TestCachedTeamFeatures(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS, WithTeamFeaturesExpiration(100*time.Millisecond))
|
|
|
|
ao := json.RawMessage(`{}`)
|
|
aq := json.RawMessage(`{"foo": "bar"}`)
|
|
testFeatures := fleet.Features{
|
|
EnableHostUsers: false,
|
|
EnableSoftwareInventory: true,
|
|
AdditionalQueries: &aq,
|
|
DetailQueryOverrides: map[string]*string{"a": ptr.String("A"), "b": ptr.String("B")},
|
|
}
|
|
|
|
testTeam := fleet.Team{
|
|
ID: 1,
|
|
CreatedAt: time.Now(),
|
|
Name: "test",
|
|
Config: fleet.TeamConfig{
|
|
Features: testFeatures,
|
|
AgentOptions: &ao,
|
|
},
|
|
}
|
|
|
|
deleted := false
|
|
mockedDS.TeamFeaturesFunc = func(ctx context.Context, teamID uint) (*fleet.Features, error) {
|
|
if deleted {
|
|
return nil, errors.New("not found")
|
|
}
|
|
return &testFeatures, nil
|
|
}
|
|
mockedDS.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return team, nil
|
|
}
|
|
mockedDS.DeleteTeamFunc = func(ctx context.Context, teamID uint) error {
|
|
deleted = true
|
|
return nil
|
|
}
|
|
|
|
// get it the first time, it will populate the cache
|
|
features, err := ds.TeamFeatures(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testFeatures, *features)
|
|
require.Same(t, &testFeatures, features)
|
|
require.True(t, mockedDS.TeamFeaturesFuncInvoked)
|
|
mockedDS.TeamFeaturesFuncInvoked = false
|
|
|
|
// get it again, will retrieve it from the cache
|
|
features, err = ds.TeamFeatures(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testFeatures, *features)
|
|
require.NotSame(t, &testFeatures, features)
|
|
require.False(t, mockedDS.TeamFeaturesFuncInvoked)
|
|
|
|
// changing e.g. the DetailQueryOverrides map doesn't affect the stored value
|
|
ptrA := features.DetailQueryOverrides["a"]
|
|
*ptrA = "AAA"
|
|
features.DetailQueryOverrides["c"] = ptr.String("C")
|
|
require.NotEqual(t, testFeatures.DetailQueryOverrides, features.DetailQueryOverrides)
|
|
|
|
// saving a team updates features in cache
|
|
aq = json.RawMessage(`{"bar": "baz"}`)
|
|
updateFeatures := fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: false,
|
|
AdditionalQueries: &aq,
|
|
DetailQueryOverrides: map[string]*string{"c": ptr.String("C")},
|
|
}
|
|
updateTeam := &fleet.Team{
|
|
ID: testTeam.ID,
|
|
CreatedAt: testTeam.CreatedAt,
|
|
Name: testTeam.Name,
|
|
Config: fleet.TeamConfig{
|
|
Features: updateFeatures,
|
|
AgentOptions: &ao,
|
|
},
|
|
}
|
|
|
|
_, err = ds.SaveTeam(context.Background(), updateTeam)
|
|
require.NoError(t, err)
|
|
require.True(t, mockedDS.SaveTeamFuncInvoked)
|
|
mockedDS.SaveTeamFuncInvoked = false
|
|
|
|
// this call gets it from the cache and gets the updated value set by SaveTeam
|
|
features, err = ds.TeamFeatures(context.Background(), testTeam.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, updateFeatures, *features)
|
|
require.NotSame(t, &updateFeatures, features)
|
|
require.False(t, mockedDS.TeamFeaturesFuncInvoked)
|
|
|
|
// deleting a team removes the features from the cache
|
|
err = ds.DeleteTeam(context.Background(), testTeam.ID)
|
|
require.NoError(t, err)
|
|
|
|
// reading hits the DB as the cached item was removed
|
|
_, err = ds.TeamFeatures(context.Background(), testTeam.ID)
|
|
require.Error(t, err)
|
|
require.True(t, mockedDS.TeamFeaturesFuncInvoked)
|
|
}
|
|
|
|
func TestCachedTeamMDMConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS, WithTeamMDMConfigExpiration(100*time.Millisecond))
|
|
ao := json.RawMessage(`{}`)
|
|
|
|
testMDMConfig := fleet.TeamMDM{
|
|
EnableDiskEncryption: true,
|
|
MacOSUpdates: fleet.MacOSUpdates{
|
|
MinimumVersion: optjson.SetString("10.10.10"),
|
|
Deadline: optjson.SetString("1992-03-01"),
|
|
},
|
|
MacOSSettings: fleet.MacOSSettings{
|
|
CustomSettings: []fleet.MDMProfileSpec{{Path: "a"}, {Path: "b"}},
|
|
DeprecatedEnableDiskEncryption: ptr.Bool(false),
|
|
},
|
|
MacOSSetup: fleet.MacOSSetup{
|
|
BootstrapPackage: optjson.SetString("bootstrap"),
|
|
},
|
|
}
|
|
|
|
testTeam := fleet.Team{
|
|
ID: 1,
|
|
CreatedAt: time.Now(),
|
|
Name: "test",
|
|
Config: fleet.TeamConfig{
|
|
MDM: testMDMConfig,
|
|
AgentOptions: &ao,
|
|
},
|
|
}
|
|
|
|
deleted := false
|
|
mockedDS.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) {
|
|
if deleted {
|
|
return nil, errors.New("not found")
|
|
}
|
|
return &testMDMConfig, nil
|
|
}
|
|
mockedDS.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return team, nil
|
|
}
|
|
mockedDS.DeleteTeamFunc = func(ctx context.Context, teamID uint) error {
|
|
deleted = true
|
|
return nil
|
|
}
|
|
|
|
// get the team's config, will load it into cache
|
|
mdmConfig, err := ds.TeamMDMConfig(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testMDMConfig, *mdmConfig)
|
|
require.Same(t, &testMDMConfig, mdmConfig)
|
|
require.True(t, mockedDS.TeamMDMConfigFuncInvoked)
|
|
mockedDS.TeamMDMConfigFuncInvoked = false
|
|
|
|
// get it again, will get it from cache
|
|
mdmConfig, err = ds.TeamMDMConfig(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testMDMConfig, *mdmConfig)
|
|
require.NotSame(t, &testMDMConfig, mdmConfig)
|
|
require.False(t, mockedDS.TeamMDMConfigFuncInvoked)
|
|
|
|
// changing some deep value doesn't affect the stored value
|
|
mdmConfig.MacOSSettings.CustomSettings[0] = fleet.MDMProfileSpec{Path: "c"}
|
|
require.NotEqual(t, testMDMConfig, *mdmConfig)
|
|
|
|
// saving a team updates config in cache
|
|
updateMDMConfig := fleet.TeamMDM{
|
|
MacOSUpdates: fleet.MacOSUpdates{
|
|
MinimumVersion: optjson.SetString("13.13.13"),
|
|
Deadline: optjson.SetString("2022-03-01"),
|
|
},
|
|
MacOSSettings: fleet.MacOSSettings{
|
|
CustomSettings: nil,
|
|
DeprecatedEnableDiskEncryption: ptr.Bool(true),
|
|
},
|
|
}
|
|
updateTeam := &fleet.Team{
|
|
ID: testTeam.ID,
|
|
CreatedAt: testTeam.CreatedAt,
|
|
Name: testTeam.Name,
|
|
Config: fleet.TeamConfig{
|
|
MDM: updateMDMConfig,
|
|
AgentOptions: &ao,
|
|
},
|
|
}
|
|
|
|
_, err = ds.SaveTeam(context.Background(), updateTeam)
|
|
require.NoError(t, err)
|
|
require.True(t, mockedDS.SaveTeamFuncInvoked)
|
|
mockedDS.SaveTeamFuncInvoked = false
|
|
|
|
// this call gets it from the cache, with the updated value set by SaveTeam
|
|
mdmConfig, err = ds.TeamMDMConfig(context.Background(), testTeam.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, updateMDMConfig, *mdmConfig)
|
|
require.NotSame(t, &updateMDMConfig, mdmConfig)
|
|
require.False(t, mockedDS.TeamMDMConfigFuncInvoked)
|
|
|
|
// deleting a team removes the config from the cache
|
|
err = ds.DeleteTeam(context.Background(), testTeam.ID)
|
|
require.NoError(t, err)
|
|
|
|
// reading hits the DB as the cached item was removed
|
|
_, err = ds.TeamMDMConfig(context.Background(), testTeam.ID)
|
|
require.Error(t, err)
|
|
require.True(t, mockedDS.TeamMDMConfigFuncInvoked)
|
|
}
|
|
|
|
func TestCachedQueryByName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS, WithQueryByNameExpiration(100*time.Millisecond))
|
|
|
|
testQuery := &fleet.Query{
|
|
ID: 1,
|
|
TeamID: ptr.Uint(1),
|
|
Packs: []fleet.Pack{{ID: 2, Name: "a"}},
|
|
}
|
|
mockedDS.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
|
|
return testQuery, nil
|
|
}
|
|
|
|
// first call gets the result from the DB
|
|
query1, err := ds.QueryByName(context.Background(), nil, "q")
|
|
require.NoError(t, err)
|
|
require.Equal(t, testQuery, query1)
|
|
require.Same(t, testQuery, query1)
|
|
require.True(t, mockedDS.QueryByNameFuncInvoked)
|
|
mockedDS.QueryByNameFuncInvoked = false
|
|
|
|
// change "stored" query.
|
|
testQuery = &fleet.Query{
|
|
ID: 1,
|
|
TeamID: ptr.Uint(2),
|
|
Packs: []fleet.Pack{{ID: 3, Name: "b"}},
|
|
}
|
|
|
|
// this call gets it from the cache
|
|
query2, err := ds.QueryByName(context.Background(), nil, "q")
|
|
require.NoError(t, err)
|
|
require.Equal(t, query1, query2) // returns the old cached value
|
|
require.NotSame(t, query1, query2) // have been cloned
|
|
require.False(t, mockedDS.QueryByNameFuncInvoked)
|
|
|
|
// a deep change doesn't alter the stored value
|
|
query2.Packs[0].Name = "Z"
|
|
require.NotEqual(t, query1, query2)
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// this call gets it from the DB again since the cache expired
|
|
query3, err := ds.QueryByName(context.Background(), nil, "q")
|
|
require.NoError(t, err)
|
|
require.Equal(t, testQuery, query3)
|
|
require.Same(t, testQuery, query3)
|
|
require.True(t, mockedDS.QueryByNameFuncInvoked)
|
|
}
|
|
|
|
func TestCachedResultCountForQuery(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS, WithQueryResultsCountExpiration(100*time.Millisecond))
|
|
|
|
testCount := 1
|
|
mockedDS.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
|
|
return testCount, nil
|
|
}
|
|
|
|
// first call gets the result from the DB
|
|
c1, err := ds.ResultCountForQuery(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testCount, c1)
|
|
require.True(t, mockedDS.ResultCountForQueryFuncInvoked)
|
|
mockedDS.ResultCountForQueryFuncInvoked = false
|
|
|
|
// change "stored" count.
|
|
testCount = 2
|
|
|
|
// this call gets it from the cache
|
|
c2, err := ds.ResultCountForQuery(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, c1, c2) // returns the old cached value
|
|
require.False(t, mockedDS.ResultCountForQueryFuncInvoked)
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// this call gets it from the DB again since the cache expired
|
|
c3, err := ds.ResultCountForQuery(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testCount, c3)
|
|
require.True(t, mockedDS.ResultCountForQueryFuncInvoked)
|
|
}
|
|
|
|
func TestGetAllMDMConfigAssetsByName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockedDS := new(mock.Store)
|
|
ds := New(mockedDS)
|
|
|
|
assetNames := []fleet.MDMAssetName{"asset1", "asset2", "asset3"}
|
|
assetMap := map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
|
"asset1": {Name: "asset1", Value: []byte("value1")},
|
|
"asset2": {Name: "asset2", Value: []byte("value2")},
|
|
"asset3": {Name: "asset2", Value: []byte("value3")},
|
|
}
|
|
assetHashes := map[fleet.MDMAssetName]string{
|
|
"asset1": "hash1",
|
|
"asset2": "hash2",
|
|
"asset3": "hash3",
|
|
}
|
|
|
|
mockedDS.GetAllMDMConfigAssetsHashesFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) {
|
|
result := map[fleet.MDMAssetName]string{}
|
|
for _, n := range assetNames {
|
|
result[n] = assetHashes[n]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
mockedDS.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
result := map[fleet.MDMAssetName]fleet.MDMConfigAsset{}
|
|
for _, n := range assetNames {
|
|
result[n] = assetMap[n]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// returns cached assets if hashes match
|
|
result, err := ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2"})
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked)
|
|
require.True(t, mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked)
|
|
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
|
|
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
|
|
require.Equal(t, assetMap["asset1"], result["asset1"])
|
|
require.Equal(t, assetMap["asset2"], result["asset2"])
|
|
require.NotContains(t, result, "asset3")
|
|
|
|
result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2"})
|
|
require.NoError(t, err)
|
|
require.False(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked)
|
|
require.True(t, mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked)
|
|
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
|
|
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
|
|
require.Equal(t, assetMap["asset1"], result["asset1"])
|
|
require.Equal(t, assetMap["asset2"], result["asset2"])
|
|
require.NotContains(t, result, "asset3")
|
|
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
|
|
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
|
|
|
|
// fetches missing assets from the db
|
|
result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2", "asset3"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, assetMap["asset1"], result["asset1"])
|
|
require.Equal(t, assetMap["asset2"], result["asset2"])
|
|
require.Equal(t, assetMap["asset3"], result["asset3"])
|
|
require.True(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked)
|
|
require.True(t, mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked)
|
|
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
|
|
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
|
|
|
|
// fetches updated assets from the db
|
|
assetHashes["asset1"] = "newhash"
|
|
assetMap["asset1"] = fleet.MDMConfigAsset{Name: "asset1", Value: []byte("newvalue")}
|
|
result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), assetNames)
|
|
require.NoError(t, err)
|
|
require.Equal(t, assetMap["asset1"], result["asset1"])
|
|
require.Equal(t, assetMap["asset2"], result["asset2"])
|
|
require.Equal(t, assetMap["asset3"], result["asset3"])
|
|
require.True(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked)
|
|
require.True(t, mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked)
|
|
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
|
|
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
|
|
|
|
// passes errors fetching assets from downstream
|
|
mockedDS.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
return nil, errors.New("error fetching assets")
|
|
}
|
|
|
|
_, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"not exists"})
|
|
require.Error(t, err)
|
|
require.Equal(t, "error fetching assets", err.Error())
|
|
|
|
// passes errors fetching hashes from downstream
|
|
mockedDS.GetAllMDMConfigAssetsHashesFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) {
|
|
return nil, errors.New("error fetching hashes")
|
|
}
|
|
|
|
_, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"not exists"})
|
|
require.Error(t, err)
|
|
require.Equal(t, "error fetching hashes", err.Error())
|
|
}
|