Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't expire iOS devices prematurely #25436

Merged
merged 5 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/25406-premature-ios-deletion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fix bug where iOS devices were being removed prematurely by expiration policy
dantecatalfamo marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion server/datastore/mysql/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3123,7 +3123,7 @@ func (ds *Datastore) CleanupExpiredHosts(ctx context.Context) ([]uint, error) {
findHostsSql := `SELECT h.id FROM hosts h
LEFT JOIN host_seen_times hst
ON h.id = hst.host_id
WHERE COALESCE(hst.seen_time, h.created_at) < DATE_SUB(NOW(), INTERVAL ? DAY)`
WHERE COALESCE(hst.seen_time, h.detail_updated_at, h.created_at) < DATE_SUB(NOW(), INTERVAL ? DAY)`
dantecatalfamo marked this conversation as resolved.
Show resolved Hide resolved

var allIdsToDelete []uint
// Process hosts using global expiry
Expand Down
87 changes: 87 additions & 0 deletions server/datastore/mysql/hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func TestHosts(t *testing.T) {
{"HostsListByDiskEncryptionStatus", testHostsListMacOSSettingsDiskEncryptionStatus},
{"HostsListFailingPolicies", printReadsInTest(testHostsListFailingPolicies)},
{"HostsExpiration", testHostsExpiration},
{"IOSHostExpiration", testIOSHostsExpiration},
{"TeamHostsExpiration", testTeamHostsExpiration},
{"HostsIncludesScheduledQueriesInPackStats", testHostsIncludesScheduledQueriesInPackStats},
{"HostsAllPackStats", testHostsAllPackStats},
Expand Down Expand Up @@ -4148,6 +4149,92 @@ func testHostsExpiration(t *testing.T, ds *Datastore) {
require.Len(t, hosts, 5)
}

func testIOSHostsExpiration(t *testing.T, ds *Datastore) {
// iOS/iPadOS devices don't have host_seen_times, meaning they
// would previously rely on created_at records for removal,
// and get deleted once the host's age was beyong the expiry
// window. We now check detail_updated_at, something that gets
// updated every time details are refetched, if seen time is
// not present.
hostExpiryWindow := 70

ac, err := ds.AppConfig(context.Background())
require.NoError(t, err)

ac.HostExpirySettings.HostExpiryEnabled = false
ac.HostExpirySettings.HostExpiryWindow = hostExpiryWindow

err = ds.SaveAppConfig(context.Background(), ac)
require.NoError(t, err)

for i := 0; i < 10; i++ {
platform := "ios"
if i%2 == 0 {
platform = "ipados"
}
detailsUpdated := time.Now()
if i >= 5 {
detailsUpdated = detailsUpdated.Add(time.Duration(-1*(hostExpiryWindow+1)*24) * time.Hour)
}

_, err := ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: detailsUpdated,
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
UUID: fmt.Sprintf("%d", i),
Hostname: fmt.Sprintf("foo.local%d", i),
Platform: platform,
})
require.NoError(t, err)
}

ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
// There are no host_seen_times for ios/ipados devices
if _, err := q.ExecContext(context.Background(), "DELETE FROM host_seen_times"); err != nil {
return err
}
// Make sure created_at is old enough to always get
// removed, we want to make sure that
// detail_updated_at is the column being checked
if _, err := q.ExecContext(context.Background(), "UPDATE hosts SET created_at = '2020-01-01 00:00:01'"); err != nil {
return err
}
return nil
})

filter := fleet.TeamFilter{User: test.UserAdmin}

hosts := listHostsCheckCount(t, ds, filter, fleet.HostListOptions{}, 10)
require.Len(t, hosts, 10)

_, err = ds.CleanupExpiredHosts(context.Background())
require.NoError(t, err)

// host expiration is still disabled
hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{}, 10)
require.Len(t, hosts, 10)

// once enabled, it works
ac.HostExpirySettings.HostExpiryEnabled = true
err = ds.SaveAppConfig(context.Background(), ac)
require.NoError(t, err)

deleted, err := ds.CleanupExpiredHosts(context.Background())
require.NoError(t, err)
require.Len(t, deleted, 5)

hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{}, 5)
require.Len(t, hosts, 5)

// And it doesn't remove more than it should
deleted, err = ds.CleanupExpiredHosts(context.Background())
require.NoError(t, err)
require.Len(t, deleted, 0)

hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{}, 5)
require.Len(t, hosts, 5)
}

func testTeamHostsExpiration(t *testing.T, ds *Datastore) {
// Set global host expiry windows
const hostExpiryWindow = 70
Expand Down
Loading