diff --git a/changes/15323-add-host_count_updated_at-to-policy b/changes/15323-add-host_count_updated_at-to-policy new file mode 100644 index 0000000000..2c48ec1396 --- /dev/null +++ b/changes/15323-add-host_count_updated_at-to-policy @@ -0,0 +1 @@ +Added `host_count_updated_at` to policy API responses. diff --git a/server/datastore/mysql/migrations/tables/20231215122713_InsertPolicyStatsData.go b/server/datastore/mysql/migrations/tables/20231215122713_InsertPolicyStatsData.go index 558463bb36..d20bea7bc7 100644 --- a/server/datastore/mysql/migrations/tables/20231215122713_InsertPolicyStatsData.go +++ b/server/datastore/mysql/migrations/tables/20231215122713_InsertPolicyStatsData.go @@ -37,6 +37,7 @@ func Up_20231215122713(tx *sql.Tx) error { WHERE p.team_id IS NULL GROUP BY p.id, t.id ON DUPLICATE KEY UPDATE + updated_at = NOW(), passing_host_count = VALUES(passing_host_count), failing_host_count = VALUES(failing_host_count); ` @@ -53,6 +54,7 @@ func Up_20231215122713(tx *sql.Tx) error { LEFT JOIN policy_membership pm ON p.id = pm.policy_id GROUP BY p.id ON DUPLICATE KEY UPDATE + updated_at = NOW(), passing_host_count = VALUES(passing_host_count), failing_host_count = VALUES(failing_host_count); ` diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go index 8baedbfec6..63b2ca6b8f 100644 --- a/server/datastore/mysql/policies.go +++ b/server/datastore/mysql/policies.go @@ -62,6 +62,7 @@ func (ds *Datastore) PolicyByName(ctx context.Context, name string) (*fleet.Poli fmt.Sprint(`SELECT `+policyCols+`, COALESCE(u.name, '') AS author_name, COALESCE(u.email, '') AS author_email, + ps.updated_at as host_count_updated_at, COALESCE(ps.passing_host_count, 0) as passing_host_count, COALESCE(ps.failing_host_count, 0) as failing_host_count FROM policies p @@ -93,6 +94,7 @@ func policyDB(ctx context.Context, q sqlx.QueryerContext, id uint, teamID *uint) SELECT %s, COALESCE(u.name, '') AS author_name, COALESCE(u.email, '') AS author_email, + ps.updated_at as host_count_updated_at, COALESCE(ps.passing_host_count, 0) as passing_host_count, COALESCE(ps.failing_host_count, 0) as failing_host_count FROM policies p @@ -312,6 +314,7 @@ func listPoliciesDB(ctx context.Context, q sqlx.QueryerContext, teamID *uint, op SELECT ` + policyCols + `, COALESCE(u.name, '') AS author_name, COALESCE(u.email, '') AS author_email, + ps.updated_at as host_count_updated_at, COALESCE(ps.passing_host_count, 0) AS passing_host_count, COALESCE(ps.failing_host_count, 0) AS failing_host_count FROM policies p @@ -348,6 +351,7 @@ func getInheritedPoliciesForTeam(ctx context.Context, q sqlx.QueryerContext, Tea ` + policyCols + `, COALESCE(u.name, '') AS author_name, COALESCE(u.email, '') AS author_email, + ps.updated_at as host_count_updated_at, COALESCE(ps.passing_host_count, 0) as passing_host_count, COALESCE(ps.failing_host_count, 0) as failing_host_count FROM policies p @@ -400,6 +404,7 @@ func (ds *Datastore) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*fl sql := `SELECT ` + policyCols + `, COALESCE(u.name, '') AS author_name, COALESCE(u.email, '') AS author_email, + ps.updated_at as host_count_updated_at, COALESCE(ps.passing_host_count, 0) as passing_host_count, COALESCE(ps.failing_host_count, 0) as failing_host_count FROM policies p @@ -1150,6 +1155,7 @@ func (ds *Datastore) UpdateHostPolicyCounts(ctx context.Context) error { WHERE p.team_id IS NULL GROUP BY p.id, t.id ON DUPLICATE KEY UPDATE + updated_at = NOW(), passing_host_count = VALUES(passing_host_count), failing_host_count = VALUES(failing_host_count); `) @@ -1169,6 +1175,7 @@ func (ds *Datastore) UpdateHostPolicyCounts(ctx context.Context) error { LEFT JOIN policy_membership pm ON p.id = pm.policy_id GROUP BY p.id ON DUPLICATE KEY UPDATE + updated_at = NOW(), passing_host_count = VALUES(passing_host_count), failing_host_count = VALUES(failing_host_count); `) diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go index 101eb6f925..2ac7856b33 100644 --- a/server/datastore/mysql/policies_test.go +++ b/server/datastore/mysql/policies_test.go @@ -2640,8 +2640,11 @@ func testUpdatePolicyHostCounts(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Equal(t, uint(0), policy.FailingHostCount) require.Equal(t, uint(0), policy.PassingHostCount) + assert.Nil(t, policy.HostCountUpdatedAt) // update policy host counts + now := time.Now().Truncate(time.Second) + later := now.Add(10 * time.Second) err = ds.UpdateHostPolicyCounts(context.Background()) require.NoError(t, err) @@ -2650,4 +2653,11 @@ func testUpdatePolicyHostCounts(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Equal(t, uint(0), policy.FailingHostCount) require.Equal(t, uint(8), policy.PassingHostCount) + require.NotNil(t, policy.HostCountUpdatedAt) + assert.True( + t, policy.HostCountUpdatedAt.Compare(now) >= 0, fmt.Sprintf("reference:%v HostCountUpdatedAt:%v", now, *policy.HostCountUpdatedAt), + ) + assert.True( + t, policy.HostCountUpdatedAt.Compare(later) < 0, fmt.Sprintf("later:%v HostCountUpdatedAt:%v", later, *policy.HostCountUpdatedAt), + ) } diff --git a/server/fleet/policies.go b/server/fleet/policies.go index d3ba98e32d..092117d9b0 100644 --- a/server/fleet/policies.go +++ b/server/fleet/policies.go @@ -3,6 +3,7 @@ package fleet import ( "errors" "strings" + "time" ) // PolicyPayload holds data for policy creation. @@ -168,7 +169,8 @@ type Policy struct { // PassingHostCount is the number of hosts this policy passes on. PassingHostCount uint `json:"passing_host_count" db:"passing_host_count"` // FailingHostCount is the number of hosts this policy fails on. - FailingHostCount uint `json:"failing_host_count" db:"failing_host_count"` + FailingHostCount uint `json:"failing_host_count" db:"failing_host_count"` + HostCountUpdatedAt *time.Time `json:"host_count_updated_at" db:"host_count_updated_at"` } func (p Policy) AuthzType() string { diff --git a/server/webhooks/failing_policies_test.go b/server/webhooks/failing_policies_test.go index aeeb7befbf..71b890cfe9 100644 --- a/server/webhooks/failing_policies_test.go +++ b/server/webhooks/failing_policies_test.go @@ -123,6 +123,7 @@ func TestTriggerFailingPoliciesWebhookBasic(t *testing.T) { "updated_at": "0001-01-01T00:00:00Z", "passing_host_count": 0, "failing_host_count": 0, + "host_count_updated_at": null, "critical": true }, "hosts": [ @@ -307,6 +308,7 @@ func TestTriggerFailingPoliciesWebhookTeam(t *testing.T) { "updated_at": "0001-01-01T00:00:00Z", "passing_host_count": 0, "failing_host_count": 0, + "host_count_updated_at": null, "critical": false }, "hosts": [