mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Fix issue with GitOps incorrectly wiping policy stats (#43282)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #43273 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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/guides/committing-changes.md#changes-files) for more information. ## Testing - [X] Added/updated automated tests - Added new test for this case (policies without software automation being pushed by two different users), verified it fails on main and passes on this branch - [X] QA'd all new/changed functionality manually - [X] Verified that changing `webhooks_and_tickets_enabled` on a policy AND running gitops as another user doesn't wipe stats - [X] Verified that changing `query` on a policy and running gitops does wipe stats - [X] Verified that changing `query` on a policy and running gitops does wipe stats <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed an issue where policy stats were incorrectly reset during GitOps policy updates. Policy statistics now remain accurate when policies are re-applied without modification to installation or script configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
38fd5edaae
commit
b2e6162e51
4 changed files with 102 additions and 14 deletions
1
43273-fix-policy-stats-wipe
Normal file
1
43273-fix-policy-stats-wipe
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
- Fixed an issue where policy stats may be wiped incorrectly after a GitOps run.
|
||||||
|
|
@ -1519,29 +1519,19 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
|
||||||
removePolicyStats bool
|
removePolicyStats bool
|
||||||
)
|
)
|
||||||
if insertOnDuplicateDidInsertOrUpdate(res) {
|
if insertOnDuplicateDidInsertOrUpdate(res) {
|
||||||
// Figure out if the query, platform, software installer, or VPP app changed.
|
// Figure out if the query, platform, software installer, VPP app, or script changed.
|
||||||
var softwareInstallerID *uint
|
|
||||||
if spec.SoftwareTitleID != nil {
|
|
||||||
softwareInstallerID = softwareInstallerIDs[teamID][*spec.SoftwareTitleID]
|
|
||||||
}
|
|
||||||
if prev, ok := teamIDToPoliciesByName[teamID][spec.Name]; ok {
|
if prev, ok := teamIDToPoliciesByName[teamID][spec.Name]; ok {
|
||||||
switch {
|
switch {
|
||||||
case prev.Query != spec.Query:
|
case prev.Query != spec.Query:
|
||||||
shouldRemoveAllPolicyMemberships = true
|
shouldRemoveAllPolicyMemberships = true
|
||||||
removePolicyStats = true
|
removePolicyStats = true
|
||||||
case teamID != nil &&
|
case teamID != nil && softwareInstallerID != nil && !ptr.Equal(prev.SoftwareInstallerID, softwareInstallerID):
|
||||||
((prev.SoftwareInstallerID == nil && spec.SoftwareTitleID != nil) ||
|
|
||||||
(prev.SoftwareInstallerID != nil && softwareInstallerID != nil && *prev.SoftwareInstallerID != *softwareInstallerID)):
|
|
||||||
shouldRemoveAllPolicyMemberships = true
|
shouldRemoveAllPolicyMemberships = true
|
||||||
removePolicyStats = true
|
removePolicyStats = true
|
||||||
case teamID != nil &&
|
case teamID != nil && vppAppsTeamsID != nil && !ptr.Equal(prev.VPPAppsTeamsID, vppAppsTeamsID):
|
||||||
((prev.VPPAppsTeamsID == nil && spec.SoftwareTitleID != nil) ||
|
|
||||||
(prev.VPPAppsTeamsID != nil && vppAppsTeamsID != nil && *prev.VPPAppsTeamsID != *vppAppsTeamsID)):
|
|
||||||
shouldRemoveAllPolicyMemberships = true
|
shouldRemoveAllPolicyMemberships = true
|
||||||
removePolicyStats = true
|
removePolicyStats = true
|
||||||
case teamID != nil &&
|
case teamID != nil && scriptID != nil && !ptr.Equal(prev.ScriptID, scriptID):
|
||||||
((prev.ScriptID == nil && spec.ScriptID != nil) ||
|
|
||||||
(prev.ScriptID != nil && spec.ScriptID != nil && *prev.ScriptID != *spec.ScriptID)):
|
|
||||||
shouldRemoveAllPolicyMemberships = true
|
shouldRemoveAllPolicyMemberships = true
|
||||||
removePolicyStats = true
|
removePolicyStats = true
|
||||||
case prev.Platforms != spec.Platform:
|
case prev.Platforms != spec.Platform:
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ func TestPolicies(t *testing.T) {
|
||||||
{"BatchedPolicyMembershipCleanupOnPolicyUpdate", testBatchedPolicyMembershipCleanupOnPolicyUpdate},
|
{"BatchedPolicyMembershipCleanupOnPolicyUpdate", testBatchedPolicyMembershipCleanupOnPolicyUpdate},
|
||||||
{"ApplyPolicySpecsNeedsFullMembershipCleanupFlag", testApplyPolicySpecsNeedsFullMembershipCleanupFlag},
|
{"ApplyPolicySpecsNeedsFullMembershipCleanupFlag", testApplyPolicySpecsNeedsFullMembershipCleanupFlag},
|
||||||
{"CleanupPolicyMembershipCrashRecovery", testCleanupPolicyMembershipCrashRecovery},
|
{"CleanupPolicyMembershipCrashRecovery", testCleanupPolicyMembershipCrashRecovery},
|
||||||
|
{"ApplyPolicySpecNoSpuriousStatsReset", testApplyPolicySpecNoSpuriousStatsReset},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
|
@ -7948,3 +7949,87 @@ func testTeamPolicyAutomationFilter(t *testing.T, ds *Datastore) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 3, mergedCount)
|
assert.Equal(t, 3, mergedCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testApplyPolicySpecNoSpuriousStatsReset verifies that re-applying a team
|
||||||
|
// policy via ApplyPolicySpecs does not spuriously reset policy_stats when the
|
||||||
|
// only change is author_id (e.g. a different user runs GitOps) and the spec
|
||||||
|
// has SoftwareTitleID set to ptr.Uint(0) (as the GitOps client does for all
|
||||||
|
// team policies to "unset" the installer field).
|
||||||
|
func testApplyPolicySpecNoSpuriousStatsReset(t *testing.T, ds *Datastore) {
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
user1 := test.NewUser(t, ds, "StatsUser1", "statsuser1@example.com", true)
|
||||||
|
user2 := test.NewUser(t, ds, "StatsUser2", "statsuser2@example.com", true)
|
||||||
|
team, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a team policy (no software installer) as user1.
|
||||||
|
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
|
||||||
|
{
|
||||||
|
Name: "stats-test-policy",
|
||||||
|
Query: "SELECT 1;",
|
||||||
|
Team: team.Name,
|
||||||
|
Platform: "darwin",
|
||||||
|
Type: fleet.PolicyTypeDynamic,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Create a host on the team and record a failing result.
|
||||||
|
hostID := fmt.Sprintf("%s-host", strings.ReplaceAll(t.Name(), "/", "_"))
|
||||||
|
h, err := ds.NewHost(ctx, &fleet.Host{
|
||||||
|
OsqueryHostID: &hostID,
|
||||||
|
DetailUpdatedAt: time.Now(),
|
||||||
|
LabelUpdatedAt: time.Now(),
|
||||||
|
PolicyUpdatedAt: time.Now(),
|
||||||
|
SeenTime: time.Now(),
|
||||||
|
NodeKey: &hostID,
|
||||||
|
UUID: hostID,
|
||||||
|
Hostname: hostID,
|
||||||
|
Platform: "darwin",
|
||||||
|
TeamID: &team.ID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Get the policy to find its ID.
|
||||||
|
policies, _, err := ds.ListTeamPolicies(ctx, team.ID, fleet.ListOptions{}, fleet.ListOptions{}, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, policies, 1)
|
||||||
|
pol := policies[0]
|
||||||
|
|
||||||
|
// Record a failing result for the host.
|
||||||
|
err = ds.RecordPolicyQueryExecutions(ctx, h, map[uint]*bool{pol.ID: new(false)}, time.Now(), false, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Update aggregate counts.
|
||||||
|
err = ds.UpdateHostPolicyCounts(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify the policy has a failing host count of 1.
|
||||||
|
policies, _, err = ds.ListTeamPolicies(ctx, team.ID, fleet.ListOptions{}, fleet.ListOptions{}, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, policies, 1)
|
||||||
|
require.Equal(t, uint(1), policies[0].FailingHostCount)
|
||||||
|
|
||||||
|
// Now re-apply the same policy as a DIFFERENT user (simulating GitOps run
|
||||||
|
// by a different API token), with SoftwareTitleID=ptr.Uint(0) as the GitOps
|
||||||
|
// client sets for all team policies. This changes author_id in the DB, which
|
||||||
|
// causes insertOnDuplicateDidInsertOrUpdate to return true. Before the fix,
|
||||||
|
// the comparison logic would see SoftwareTitleID!=nil and incorrectly
|
||||||
|
// conclude the software installer changed, resetting stats.
|
||||||
|
require.NoError(t, ds.ApplyPolicySpecs(ctx, user2.ID, []*fleet.PolicySpec{
|
||||||
|
{
|
||||||
|
Name: "stats-test-policy",
|
||||||
|
Query: "SELECT 1;",
|
||||||
|
Team: team.Name,
|
||||||
|
Platform: "darwin",
|
||||||
|
SoftwareTitleID: new(uint), // ptr to 0, as GitOps does
|
||||||
|
Type: fleet.PolicyTypeDynamic,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Verify that policy stats were NOT reset.
|
||||||
|
policies, _, err = ds.ListTeamPolicies(ctx, team.ID, fleet.ListOptions{}, fleet.ListOptions{}, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, policies, 1)
|
||||||
|
assert.Equal(t, uint(1), policies[0].FailingHostCount, "policy stats should not have been reset")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,18 @@ func T[T any](x T) *T {
|
||||||
return &x
|
return &x
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equal returns true if both pointers are nil, or both are non-nil and
|
||||||
|
// point to equal values.
|
||||||
|
func Equal[T comparable](a, b *T) bool {
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return *a == *b
|
||||||
|
}
|
||||||
|
|
||||||
// ValOrZero returns the value of x if x is not nil, and the zero value
|
// ValOrZero returns the value of x if x is not nil, and the zero value
|
||||||
// for T otherwise.
|
// for T otherwise.
|
||||||
func ValOrZero[T any](x *T) T {
|
func ValOrZero[T any](x *T) T {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue