Skip to content

Preserve android device team assignment - #46868

Merged
ksykulev merged 5 commits into
mainfrom
45263-android-transfer
Jun 5, 2026
Merged

Preserve android device team assignment#46868
ksykulev merged 5 commits into
mainfrom
45263-android-transfer

Conversation

@ksykulev

@ksykulev ksykulev commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Related issue: Resolves #45263

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.
  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.

Testing

Summary by CodeRabbit

  • Bug Fixes
    • Android devices no longer lose team assignments or certificate configuration when a host is deleted and the device re-enrolls.
    • Re-enrollment restores a device’s previously known team when available, preserving certificate templates and team-specific settings.
    • Team transfers for Android devices now reliably update device records so certificates and access remain consistent.

@ksykulev
ksykulev requested a review from a team as a code owner June 4, 2026 22:38
Copilot AI review requested due to automatic review settings June 4, 2026 22:38

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@ksykulev ksykulev changed the title Persist android device team assignment Preserve android device team assignment Jun 4, 2026
@ksykulev
ksykulev force-pushed the 45263-android-transfer branch from ccb6f9c to 9c816d3 Compare June 4, 2026 22:40
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds persistent team_id tracking for Android devices in android_devices, backfills the column in a migration, and updates datastore, enrollment, and transfer flows to read or write that value. Host deletion no longer removes android_devices rows, and Android re-enrollment now restores a previously known team when one exists. Transfer paths also synchronize android_devices.team_id to the destination team. Tests, mocks, and a changelog entry were updated.

Possibly related PRs

  • fleetdm/fleet#46759: Modifies Android host transfer and pending certificate-template handling in server/service/hosts.go, which overlaps with the same transfer path updated here.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Preserve android device team assignment' accurately summarizes the main objective and is directly reflected in the changes throughout the PR.
Description check ✅ Passed The PR description checks key boxes (changes file, validation, tests, QA) aligned with the template and links issue #45263, though some optional sections are omitted.
Linked Issues check ✅ Passed All code changes directly implement the stated objective: preserve Android device team assignment across host record deletion/re-enrollment by storing team_id in android_devices table.
Out of Scope Changes check ✅ Passed All changes are scoped to preserving Android device team assignment (database schema, datastore methods, re-enrollment logic, and tests), with no extraneous modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 45263-android-transfer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Fleet Android MDM re-enrollment regression by persisting an Android device’s last-known team assignment (and using it during subsequent enrollments) so that team-scoped certificate template delivery can resume automatically after host record churn.

Changes:

  • Persist team_id on android_devices (migration + schema snapshot) and sync it from the host record.
  • Restore the last-known Android team during enrollment/re-enrollment (prefer persisted team over enroll secret default).
  • Preserve android_devices rows across host deletion and extend mocks to support the new datastore method.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
server/mock/datastore_mock.go Adds a mock hook for GetAndroidDeviceLastTeamID.
server/mdm/android/service/pubsub.go Uses last-known Android team during enrollment and adjusts cert-template team selection.
server/mdm/android/service/enterprises_test.go Initializes the new mock method in common Android service mocks.
server/fleet/datastore.go Extends AndroidDatastore interface with GetAndroidDeviceLastTeamID.
server/datastore/mysql/schema.sql Adds android_devices.team_id + FK to teams and updates schema snapshot metadata.
server/datastore/mysql/migrations/tables/20260604221206_AddTeamIDToAndroidDevices.go Migration adding/backfilling android_devices.team_id.
server/datastore/mysql/migrations/tables/20260604221206_AddTeamIDToAndroidDevices_test.go Migration test verifying backfill + ON DELETE SET NULL.
server/datastore/mysql/hosts.go Stops deleting android_devices rows when hosts are deleted.
server/datastore/mysql/android.go Syncs android_devices.team_id and implements GetAndroidDeviceLastTeamID.
changes/45263-android-transfer-bug User-visible changelog entry for the fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/datastore/mysql/hosts.go Outdated
Comment thread server/fleet/datastore.go
Comment thread server/mdm/android/service/pubsub.go Outdated
Comment thread server/mdm/android/service/pubsub.go Outdated
@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.44578% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.02%. Comparing base (99ebcf3) to head (aa5f4b8).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
server/datastore/mysql/android.go 71.42% 6 Missing and 4 partials ⚠️
server/mdm/android/service/pubsub.go 50.00% 5 Missing and 5 partials ⚠️
...tables/20260604221206_AddTeamIDToAndroidDevices.go 57.14% 4 Missing and 2 partials ⚠️
server/service/hosts.go 60.00% 2 Missing and 2 partials ⚠️
server/mdm/android/tests/testing_utils.go 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #46868      +/-   ##
==========================================
+ Coverage   67.01%   67.02%   +0.01%     
==========================================
  Files        2858     2855       -3     
  Lines      224622   224767     +145     
  Branches    11595    11581      -14     
==========================================
+ Hits       150524   150653     +129     
- Misses      60449    60451       +2     
- Partials    13649    13663      +14     
Flag Coverage Δ
backend 68.73% <61.44%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.

Comment thread server/service/hosts_test.go Outdated
Comment thread server/service/hosts_test.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
server/datastore/mysql/android_test.go (1)

3217-3225: ⚡ Quick win

Assert host2 remains assigned after clearing host1.

After clearing team for h1, add an assertion that h2 is still on team.ID. This test currently misses regressions where UPDATE scope is broader than intended.

🧪 Suggested assertion add
 	ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
 		return sqlx.GetContext(ctx, q, &teamID1, `SELECT team_id FROM android_devices WHERE host_id = ?`, h1.Host.ID)
 	})
 	require.Nil(t, teamID1)
+
+	ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+		return sqlx.GetContext(ctx, q, &teamID2, `SELECT team_id FROM android_devices WHERE host_id = ?`, h2.Host.ID)
+	})
+	require.NotNil(t, teamID2)
+	require.Equal(t, team.ID, *teamID2)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/datastore/mysql/android_test.go` around lines 3217 - 3225, Add an
assertion that h2 remains assigned to team after clearing h1: after the
UpdateAndroidDeviceTeamID call and the existing check that h1's team_id is nil,
query the android_devices row for h2 (use ExecAdhocSQL/ sqlx.GetContext like the
existing query and read into a new variable e.g. teamID2) and require that
teamID2 equals team.ID (use require.Equal or require.NotNil+require.Equal) so
the test ensures UpdateAndroidDeviceTeamID(h1) didn't affect h2.
server/service/hosts_test.go (1)

1973-1975: ⚡ Quick win

Assert UpdateAndroidDeviceTeamID invocation and arguments in these transfer tests.

These new stubs only suppress unexpected-call failures; they don’t validate that Android team sync actually happens with the expected UUIDs and destination team. Please add assertions (invoked + expected hostUUIDs/teamID) so regressions in transfer-side team persistence are caught.

Suggested test hardening
 ds.UpdateAndroidDeviceTeamIDFunc = func(ctx context.Context, hostUUIDs []string, teamID *uint) error {
+   require.Equal(t, []string{"android-uuid-1"}, hostUUIDs)
+   require.NotNil(t, teamID)
+   require.Equal(t, uint(5), *teamID)
    return nil
 }

Based on learnings from the PR objectives/review context, this path is expected to preserve Android team assignment during transfer and should be explicitly verified in tests.

Also applies to: 2051-2053

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/service/hosts_test.go` around lines 1973 - 1975, The test currently
stubs ds.UpdateAndroidDeviceTeamIDFunc to always return nil which hides whether
Android device team syncing ran; change the stub
(ds.UpdateAndroidDeviceTeamIDFunc) to record that it was invoked and capture its
hostUUIDs and teamID parameters (e.g., store into local variables and a called
bool), then after the transfer action assert the function was called and that
the captured hostUUIDs match the expected UUID list and that the captured teamID
pointer equals the expected destination team ID (including nil vs non-nil
handling); apply the same pattern to the other stubs referenced around lines
2051-2053 so all transfer tests explicitly verify UpdateAndroidDeviceTeamID was
invoked with correct args.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/datastore/mysql/android.go`:
- Around line 339-346: In UpdateAndroidDeviceTeamID, avoid passing empty UUIDs
into the sqlx.In IN clause by filtering hostUUIDs before building the query:
remove any empty or all-whitespace strings (e.g., trim and drop ""), then if the
filtered slice is empty return nil; otherwise use the filtered slice in the
sqlx.In call so the WHERE h.uuid IN (?) is precisely scoped to intended hosts
and cannot match hosts with an empty uuid.

---

Nitpick comments:
In `@server/datastore/mysql/android_test.go`:
- Around line 3217-3225: Add an assertion that h2 remains assigned to team after
clearing h1: after the UpdateAndroidDeviceTeamID call and the existing check
that h1's team_id is nil, query the android_devices row for h2 (use
ExecAdhocSQL/ sqlx.GetContext like the existing query and read into a new
variable e.g. teamID2) and require that teamID2 equals team.ID (use
require.Equal or require.NotNil+require.Equal) so the test ensures
UpdateAndroidDeviceTeamID(h1) didn't affect h2.

In `@server/service/hosts_test.go`:
- Around line 1973-1975: The test currently stubs
ds.UpdateAndroidDeviceTeamIDFunc to always return nil which hides whether
Android device team syncing ran; change the stub
(ds.UpdateAndroidDeviceTeamIDFunc) to record that it was invoked and capture its
hostUUIDs and teamID parameters (e.g., store into local variables and a called
bool), then after the transfer action assert the function was called and that
the captured hostUUIDs match the expected UUID list and that the captured teamID
pointer equals the expected destination team ID (including nil vs non-nil
handling); apply the same pattern to the other stubs referenced around lines
2051-2053 so all transfer tests explicitly verify UpdateAndroidDeviceTeamID was
invoked with correct args.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8c458e29-0870-49f6-bc1e-529f943a91e7

📥 Commits

Reviewing files that changed from the base of the PR and between 9c816d3 and 0c74bf7.

📒 Files selected for processing (10)
  • server/datastore/mysql/android.go
  • server/datastore/mysql/android_test.go
  • server/datastore/mysql/migrations/tables/20260604221206_AddTeamIDToAndroidDevices_test.go
  • server/fleet/datastore.go
  • server/mdm/android/service/pubsub.go
  • server/mdm/android/service/pubsub_test.go
  • server/mdm/android/tests/testing_utils.go
  • server/mock/datastore_mock.go
  • server/service/hosts.go
  • server/service/hosts_test.go
✅ Files skipped from review due to trivial changes (1)
  • server/mock/datastore_mock.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/fleet/datastore.go
  • server/mdm/android/service/pubsub.go

Comment thread server/datastore/mysql/android.go Outdated

@getvictor getvictor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a few comments.

Note: I don't think other platforms re-enroll into the same team as before. But it makes sense for Android so we don't churn the agent state.

Comment thread server/datastore/mysql/hosts.go Outdated
Comment on lines +619 to +621
// - android_devices: preserved so the device's last-known team_id survives host
// deletion and is available during re-enrollment. CreateDeviceTx finds the
// existing row by enterprise_specific_id and reuses it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the Android device is disconnected from MDM and then deleted, we should delete this row then, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't delete the row on unenrollment. The device may re-enroll, and the whole point of this fix is preserving state across that cycle. As far as I can tell, Apple does exactly the same thing, their nano_devices, nano_enrollments, and host_dep_assignments rows all persist across host deletion. The host_dep_assignments table has a deleted_at soft-delete column, when a device is removed from ABM, the row gets a timestamp but isn't actually deleted.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant when device fist unenrolls, and then we also delete it. I guess if the device re-enrolls with the same enterprise, it will still have the same id.

Comment thread server/mdm/android/service/pubsub.go Outdated
Comment thread server/fleet/datastore.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/datastore/mysql/android.go (1)

358-363: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the primary for this last-team lookup.

This helper feeds the re-enrollment restore path in server/mdm/android/service/pubsub.go:625-643, but it reads from ds.reader(ctx) while the corresponding team syncs write through ds.writer(ctx). Under replica lag, a device transferred or updated just before re-enrollment can restore a stale or nil team_id and come back on the wrong team, which breaks the persistence guarantee this PR is adding.

Suggested fix
 func (ds *Datastore) GetAndroidDeviceLastTeamID(ctx context.Context, enterpriseSpecificID string) (*uint, bool, error) {
 	var teamID *uint
-	err := sqlx.GetContext(ctx, ds.reader(ctx), &teamID,
+	err := sqlx.GetContext(ctx, ds.writer(ctx), &teamID,
 		`SELECT team_id FROM android_devices WHERE enterprise_specific_id = ?`,
 		enterpriseSpecificID,
 	)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/datastore/mysql/android.go` around lines 358 - 363,
GetAndroidDeviceLastTeamID currently reads from the read-replica via
ds.reader(ctx) which can return stale team_id; change it to use the primary
writer (ds.writer(ctx)) so the SELECT reads from the authoritative DB (adjust
the sqlx.GetContext call to use ds.writer(ctx) in the GetAndroidDeviceLastTeamID
function) to prevent replica-lag stale results when re-enrolling devices.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@server/datastore/mysql/android.go`:
- Around line 358-363: GetAndroidDeviceLastTeamID currently reads from the
read-replica via ds.reader(ctx) which can return stale team_id; change it to use
the primary writer (ds.writer(ctx)) so the SELECT reads from the authoritative
DB (adjust the sqlx.GetContext call to use ds.writer(ctx) in the
GetAndroidDeviceLastTeamID function) to prevent replica-lag stale results when
re-enrolling devices.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 70c99736-c357-4755-9d4e-a82d6cd24ca7

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa8edc and aa5f4b8.

📒 Files selected for processing (9)
  • server/datastore/mysql/android.go
  • server/datastore/mysql/android_test.go
  • server/datastore/mysql/hosts.go
  • server/fleet/datastore.go
  • server/mdm/android/service/pubsub.go
  • server/mdm/android/tests/testing_utils.go
  • server/mock/datastore_mock.go
  • server/service/hosts.go
  • server/service/hosts_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • server/mdm/android/tests/testing_utils.go
  • server/fleet/datastore.go
  • server/datastore/mysql/android_test.go
  • server/mock/datastore_mock.go
  • server/service/hosts.go
  • server/mdm/android/service/pubsub.go

@ksykulev
ksykulev requested a review from getvictor June 5, 2026 17:53

@getvictor getvictor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ksykulev
ksykulev merged commit b02fa18 into main Jun 5, 2026
42 checks passed
@ksykulev
ksykulev deleted the 45263-android-transfer branch June 5, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android certificate template state and team assignment are lost when the host record is deleted and the device re-enrolls

3 participants