Skip to content

SyncML <Delete> Windows profiles - #42206

Merged
getvictor merged 28 commits into
mainfrom
victor/33418-windows-delete
Mar 26, 2026
Merged

SyncML <Delete> Windows profiles#42206
getvictor merged 28 commits into
mainfrom
victor/33418-windows-delete

Conversation

@getvictor

@getvictor getvictor commented Mar 21, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #33418

Demo video: https://www.youtube.com/watch?v=gtsIYxmIOSo
Docs: https://github.com/fleetdm/fleet/pull/42269/changes

Checklist for submitter

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.

Testing

Summary by CodeRabbit

  • New Features

    • Windows profiles now send SyncML commands when profiles are removed or hosts change teams, ensuring profile settings are removed from devices like on macOS.
    • Deletion is handled as a two-phase flow: pending removals are enqueued and tracked instead of being immediately deleted.
  • Tests

    • Added/updated tests for delete-command generation, remove-status mappings, and end-to-end removal reconciliation.

@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

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

Adds Windows profile “removal” support by generating/enqueuing SyncML <Delete> commands when profiles are removed from a host or deleted, bringing Windows behavior closer to macOS profile removal.

Changes:

  • Extend Windows profile reconciliation to enqueue <Delete> commands for profiles that should be removed from hosts.
  • Add SyncML delete-command generation + remove-specific delivery status mapping (treat 404/405 as success for remove).
  • Update datastore profile-deletion flows to read stored SyncML before deletion and attempt best-effort removal.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
server/service/microsoft_mdm.go Reconciler now builds “remove” targets and enqueues delete commands.
server/fleet/microsoft_mdm.go Implements delete command generation and remove-specific status mapping.
server/fleet/microsoft_mdm_test.go Adds unit tests for delete command generation and remove-status handling.
server/datastore/mysql/microsoft_mdm.go Two-phase deletion logic + operation-type aware response handling + mark-for-remove updates.
changes/33418-windows-mdm-profile-deletion Release note for Windows profile deletion behavior.

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

Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/service/microsoft_mdm.go
Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/fleet/microsoft_mdm.go Outdated
Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds Windows MDM profile removal: when a Windows configuration profile is deleted or a host changes teams, Fleet reads stored profile SyncML, generates corresponding SyncML <Delete> commands, and enqueues them for affected hosts. The datastore uses a two‑phase cleanup: never‑sent install rows are deleted, delivered installs are marked operation_type=remove and set pending; terminal remove rows are cleaned up after verification. New code builds delete commands from profile bytes, maps remove response codes (treating NotFound/NotAllowed as verified), and stops retrying failed remove operations. A changelog entry was added.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description is incomplete and missing several required sections from the template, including validation, database migrations, and new configuration settings checklists. Complete the remaining unchecked sections: add details about input validation/SQL injection prevention, database migration checks, new configuration settings (if applicable), and fleetd/orbit compatibility verification. Alternatively, delete non-applicable sections as instructed in the template.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'SyncML Windows profiles' clearly and specifically describes the main technical change: implementing deletion of Windows configuration profiles via SyncML Delete commands.
Linked Issues check ✅ Passed The changes comprehensively implement the engineering plan from #33418: two-phase datastore deletion, SyncML Delete command generation, reconciler updates, 405 response handling as success, and delete command generation at deletion time. All coding requirements are met.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #33418 objectives. File modifications include datastore logic, SyncML command generation, reconciliation, response handling, tests, and test utilities—all scoped to Windows profile deletion feature.

✏️ 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 victor/33418-windows-delete

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.

@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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/datastore/mysql/microsoft_mdm.go`:
- Around line 1023-1062: The update loop unconditionally flips
host_mdm_windows_profiles to remove/pending and sets a new command_uuid even
when no delete command was enqueued; fix this by tracking which profiles
actually had a delete command inserted and only update those. In the first loop
(where you read profileContents, call fleet.BuildDeleteCommandFromProfileBytes
and ds.mdmWindowsInsertCommandForHostsDB) record successful enqueues (e.g., add
profUUID to a set or mark target with a flag) and skip adding to that set when
syncML is missing or BuildDeleteCommandFromProfileBytes or
mdmWindowsInsertCommandForHostsDB returns an error. In the second loop (the
sqlx.In update block) iterate only over the profUUIDs that were successfully
enqueued (the tracked set or flagged targets) so you only set operation_type =
remove, status = pending and command_uuid for profiles that actually have a
corresponding delete command.
- Around line 986-1002: The SELECT used in selectSentStmt currently only returns
rows with status IS NOT NULL and operation_type = fleet.MDMOperationTypeInstall,
which misses hosts that already have a pending remove (operation_type='remove')
with NULL status; change the WHERE clause to include either sent installs OR any
remove rows by using a compound condition such as "WHERE profile_uuid IN (?) AND
((status IS NOT NULL AND operation_type = ?) OR (operation_type = ?))", adjust
the sqlx.In call to pass profileUUIDs, fleet.MDMOperationTypeInstall,
fleet.MDMOperationTypeRemove as selArgs, and keep the rest of the code using
selStmt/selArgs and sentRows unchanged so removed-but-not-yet-statused hosts are
included.

In `@server/fleet/microsoft_mdm.go`:
- Around line 1719-1726: BuildDeleteCommandFromProfileBytes currently parses the
raw stored SyncML which can produce multiple top-level <Delete> commands for
SCEP cert profiles; mirror the install-side behavior by normalizing SCEP
payloads to ensure they are wrapped in an <Atomic> element before parsing.
Update BuildDeleteCommandFromProfileBytes to call a new helper (e.g.,
NormalizeSCEPPayloadAtomic) on profileBytes prior to
UnmarshallMultiTopLevelXMLProfile so non-atomic SCEP payloads are wrapped into a
single atomic envelope; keep references to BuildDeleteCommandFromProfileBytes
and UnmarshallMultiTopLevelXMLProfile so reviewers can locate the change. Ensure
the normalization only alters SCEP payloads and preserves other profiles' XML
unchanged.
- Around line 1601-1614: The builder that processes
UnmarshallMultiTopLevelXMLProfile(cmdWithSecret.RawCommand) sets statusMapper
based on isRemoveOperation but the per-target failed-atomic detail logic only
inspects ReplaceCommands and AddCommands, so failed Delete (atomic remove)
responses produce empty Detail; update the per-target detail construction to
also inspect cmds.DeleteCommands (in the same place ReplaceCommands and
AddCommands are checked) and include DeleteCommands entries when
isRemoveOperation so failed removes populate the Detail field correctly (ensure
any indexing/ID lookups and merging logic mirror how ReplaceCommands/AddCommands
are handled).

In `@server/service/microsoft_mdm.go`:
- Around line 2598-2623: The loop that builds remove targets (iterating over
toRemove and using removeTargets, cmdTarget, and hostProfilesToUpdate) currently
marks host profiles as pending (appends hp with Status = MDMDeliveryPending and
sets CommandUUID) before the delete command is actually generated/queued;
instead, only create and append the hostProfilesToUpdate entry after
BuildDeleteCommandFromProfileBytes succeeds and the command has been
enqueued/assigned a real command UUID. Concretely: defer creating the hp and
setting Status/CommandUUID inside the remove loop until after successful call to
BuildDeleteCommandFromProfileBytes (and after the code path that queues the
command and confirms the command UUID), update cmdTarget.cmdUUID only when the
command is guaranteed queued, and apply the same change to the equivalent block
referenced by the other occurrence that mirrors lines using
toRemove/removeTargets (the block around the later 2741-2757 section).
🪄 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: 3e9fe774-b2ff-4aee-8847-3767c7777f77

📥 Commits

Reviewing files that changed from the base of the PR and between 25455df and d4b7af2.

📒 Files selected for processing (5)
  • changes/33418-windows-mdm-profile-deletion
  • server/datastore/mysql/microsoft_mdm.go
  • server/fleet/microsoft_mdm.go
  • server/fleet/microsoft_mdm_test.go
  • server/service/microsoft_mdm.go

Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/datastore/mysql/microsoft_mdm.go
Comment thread server/fleet/microsoft_mdm.go
Comment thread server/fleet/microsoft_mdm.go Outdated
Comment thread server/service/microsoft_mdm.go Outdated
@codecov

codecov Bot commented Mar 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.51163% with 98 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.70%. Comparing base (0c4e4e4) to head (3e05605).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
server/datastore/mysql/microsoft_mdm.go 63.00% 47 Missing and 27 partials ⚠️
server/service/microsoft_mdm.go 74.00% 7 Missing and 6 partials ⚠️
server/fleet/microsoft_mdm.go 90.66% 4 Missing and 3 partials ⚠️
server/datastore/mysql/teams.go 77.77% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #42206      +/-   ##
==========================================
+ Coverage   66.63%   66.70%   +0.06%     
==========================================
  Files        2532     2524       -8     
  Lines      202579   202661      +82     
  Branches     9027     8966      -61     
==========================================
+ Hits       134979   135175     +196     
+ Misses      55397    55248     -149     
- Partials    12203    12238      +35     
Flag Coverage Δ
backend 68.48% <71.51%> (+0.01%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

- Address code review: SCEP atomic normalization for delete, failed
  atomic detail includes DeleteCommands, only update rows when delete
  command was actually enqueued, batch UPDATE for mark-for-removal,
  if-else→switch lint fix, remove unused const.
- Fix install query to re-install profiles currently marked for removal
  but back in desired state.
- Fix test assertions to account for two-phase Windows profile removal.
@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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 8 out of 9 changed files in this pull request and generated 7 comments.


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

Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/service/microsoft_mdm.go Outdated
Comment thread server/datastore/mysql/mdm_test.go Outdated
Comment thread server/datastore/mysql/microsoft_mdm_test.go

@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: 3

Caution

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

⚠️ Outside diff range comments (2)
server/service/integration_mdm_profiles_test.go (1)

4026-4050: ⚠️ Potential issue | 🟠 Major

Don’t infer “delete” from the absence of ReplaceCommands.

Add-only Windows installs are valid, and this file already exercises them elsewhere. Classifying every Atomic without ReplaceCommands as a remove will undercount installs, skip their DB-status assertions, and make this helper return the wrong result as soon as an Add-only profile flows through it.

🛠️ Suggested fix
 		for _, c := range cmds {
 			cmdID := c.Cmd.CmdID
 			status := syncml.CmdStatusOK
 			if c.Verb == "Atomic" {
-				if len(c.Cmd.ReplaceCommands) > 0 {
-					// Install command (Atomic with Replace sub-commands)
+				if len(c.Cmd.DeleteCommands) > 0 {
+					// Delete command (Atomic with Delete sub-commands)
+					status = syncml.CmdStatusOK
+				} else if len(c.Cmd.ReplaceCommands) > 0 || len(c.Cmd.AddCommands) > 0 {
+					// Install command (Atomic with Replace/Add sub-commands)
 					atomicInstallCmds = append(atomicInstallCmds, c)
 					status = mdmResponseStatus
 					for _, rc := range c.Cmd.ReplaceCommands {
 						require.NotEmpty(t, rc.CmdID)
 					}
-				} else {
-					// Delete command (Atomic with Delete sub-commands)
-					status = syncml.CmdStatusOK
+					for _, ac := range c.Cmd.AddCommands {
+						require.NotEmpty(t, ac.CmdID)
+					}
+				} else {
+					require.Fail(t, "unexpected Atomic command without install or delete subcommands")
 				}
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/service/integration_mdm_profiles_test.go` around lines 4026 - 4050,
The code currently treats any Atomic command without ReplaceCommands as a
delete; instead check for explicit delete sub-commands and treat absence of
ReplaceCommands as an add-only install. Change the branch in the loop that
inspects c.Verb == "Atomic" to: if len(c.Cmd.ReplaceCommands) > 0 -> handle
install (append to atomicInstallCmds and set status = mdmResponseStatus); else
if len(c.Cmd.DeleteCommands) > 0 -> handle delete (set status =
syncml.CmdStatusOK); else -> treat as add-only install (append to
atomicInstallCmds and set status = mdmResponseStatus). Update references to
c.Cmd.DeleteCommands, atomicInstallCmds, status, mdmResponseStatus, and
syncml.CmdStatusOK so add-only Atomics are counted and get DB-status assertions.
server/fleet/microsoft_mdm.go (1)

1619-1625: ⚠️ Potential issue | 🟠 Major

Best-effort remove doesn't apply inside <Atomic>.

This branch still derives the profile status from the wrapper status only, so nested <Delete> 404/405 responses never participate in the overall result. Atomic/SCEP removals can therefore stay failed even when each child delete should count as success for best-effort removal. For remove operations, the atomic path should aggregate the nested command statuses too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/fleet/microsoft_mdm.go` around lines 1619 - 1625, The atomic branch
currently sets commandStatus from only the wrapper status (using
statuses[cmdWithSecret.CommandUUID]); change it so remove operations inside an
<Atomic> aggregate child command statuses instead: iterate the nested child
commands present in cmds (the Atomic wrapper's children), look up each child's
status in the statuses map, map each child's status via
statusMapper(*status.Data), and combine them with best-effort semantics (treat
child Delete 404/405 as success) to produce the overall commandStatus; ensure
you still fall back to the wrapper status only if child statuses are missing.
Use the existing symbols cmds, cmdWithSecret.CommandUUID, statuses,
statusMapper, and commandStatus to locate and implement the aggregation.
♻️ Duplicate comments (1)
server/datastore/mysql/microsoft_mdm.go (1)

992-1006: ⚠️ Potential issue | 🟠 Major

Don't drop already-pending removes during profile deletion.

Phase 0 deletes every existing operation_type='remove' row, then Phase 2 reloads only sent installs. If a host was already marked remove with status=NULL because it left scope earlier, this path erases the only row for that host and the later reconcile pass has no stored SyncML left to build the <Delete> from. Keep pending remove rows in the Phase 2 source set, or restrict Phase 0 cleanup to terminal remove rows only.

As per coding guidelines, when reviewing SQL queries that are added or modified, ensure that appropriate filtering criteria are applied—especially when a query is intended to return data for a specific entity.

Also applies to: 1021-1027

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/datastore/mysql/microsoft_mdm.go` around lines 992 - 1006, The Phase 0
cleanup currently deletes all rows with operation_type =
fleet.MDMOperationTypeRemove via delExistingRemoveStmt which removes pending
removes (status IS NULL); change the SQL to only delete terminal remove rows so
pending removes are preserved for Phase 2. Modify delExistingRemoveStmt to add a
filter (e.g., "AND status IS NOT NULL" or specific terminal status values) so
that delRemStmt/delRemArgs and the tx.ExecContext call only remove terminal
remove rows, not rows with status NULL.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/datastore/mysql/mdm_test.go`:
- Around line 1338-1380: The assertHostProfiles helper is becoming destructive
because it calls cleanupStaleWindowsRemoveRows; remove that call so
assertHostProfiles remains read-only and only verifies DB state. Instead,
advance Windows remove-phase state explicitly from tests by calling the existing
simulateWindowsRemoveReconciliation (or a new explicit cleanup helper) at the
specific phase boundaries that are meant to reconcile/remove Windows remove
rows; keep cleanupStaleWindowsRemoveRows as an explicit utility invoked only by
tests that intend to advance the Windows remove lifecycle. Ensure references to
cleanupStaleWindowsRemoveRows and simulateWindowsRemoveReconciliation are used
from the test phases, not inside assertHostProfiles.

In `@server/datastore/mysql/microsoft_mdm.go`:
- Around line 953-959: The transaction currently deletes the MDM profile row
before attempting to build or persist the SyncML/delete payload, so failures in
SyncML generation (or in cancelWindowsHostInstallsForDeletedMDMProfiles) leave
the DB row gone but no persisted payload to retry; change the flow so you build
and/or persist the delete payload(s) first and call
cancelWindowsHostInstallsForDeletedMDMProfiles (or return its error) before
calling deleteMDMWindowsConfigProfile, or alternatively make
deleteMDMWindowsConfigProfile persist the delete payload atomically with the row
removal; ensure any SyncML/build errors cause the transaction to return an error
(abort) rather than log a warning and continue — apply the same fix to the
similar block around lines 1063-1077.
- Around line 948-951: The preload SELECT using sqlx.GetContext currently
returns raw sql.ErrNoRows which prevents deleteMDMWindowsConfigProfile from
hitting its notFound("MDMWindowsProfile") path; change the preload to detect
sql.ErrNoRows (the error returned by sqlx.GetContext) and, when that occurs,
return the same notFound("MDMWindowsProfile") error that
deleteMDMWindowsConfigProfile expects so missing profiles still result in a
404—locate the sqlx.GetContext(..., &syncML, `SELECT syncml ...`, profileUUID)
call and branch on sql.ErrNoRows to return notFound("MDMWindowsProfile") instead
of wrapping/returning the raw DB error.

---

Outside diff comments:
In `@server/fleet/microsoft_mdm.go`:
- Around line 1619-1625: The atomic branch currently sets commandStatus from
only the wrapper status (using statuses[cmdWithSecret.CommandUUID]); change it
so remove operations inside an <Atomic> aggregate child command statuses
instead: iterate the nested child commands present in cmds (the Atomic wrapper's
children), look up each child's status in the statuses map, map each child's
status via statusMapper(*status.Data), and combine them with best-effort
semantics (treat child Delete 404/405 as success) to produce the overall
commandStatus; ensure you still fall back to the wrapper status only if child
statuses are missing. Use the existing symbols cmds, cmdWithSecret.CommandUUID,
statuses, statusMapper, and commandStatus to locate and implement the
aggregation.

In `@server/service/integration_mdm_profiles_test.go`:
- Around line 4026-4050: The code currently treats any Atomic command without
ReplaceCommands as a delete; instead check for explicit delete sub-commands and
treat absence of ReplaceCommands as an add-only install. Change the branch in
the loop that inspects c.Verb == "Atomic" to: if len(c.Cmd.ReplaceCommands) > 0
-> handle install (append to atomicInstallCmds and set status =
mdmResponseStatus); else if len(c.Cmd.DeleteCommands) > 0 -> handle delete (set
status = syncml.CmdStatusOK); else -> treat as add-only install (append to
atomicInstallCmds and set status = mdmResponseStatus). Update references to
c.Cmd.DeleteCommands, atomicInstallCmds, status, mdmResponseStatus, and
syncml.CmdStatusOK so add-only Atomics are counted and get DB-status assertions.

---

Duplicate comments:
In `@server/datastore/mysql/microsoft_mdm.go`:
- Around line 992-1006: The Phase 0 cleanup currently deletes all rows with
operation_type = fleet.MDMOperationTypeRemove via delExistingRemoveStmt which
removes pending removes (status IS NULL); change the SQL to only delete terminal
remove rows so pending removes are preserved for Phase 2. Modify
delExistingRemoveStmt to add a filter (e.g., "AND status IS NOT NULL" or
specific terminal status values) so that delRemStmt/delRemArgs and the
tx.ExecContext call only remove terminal remove rows, not rows with status NULL.
🪄 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: 567675cd-3fe4-4f5e-a6d4-8afd31d77f85

📥 Commits

Reviewing files that changed from the base of the PR and between d4b7af2 and 7a96c0c.

📒 Files selected for processing (7)
  • server/datastore/mysql/mdm_test.go
  • server/datastore/mysql/microsoft_mdm.go
  • server/datastore/mysql/microsoft_mdm_test.go
  • server/datastore/mysql/testing_utils.go
  • server/fleet/microsoft_mdm.go
  • server/service/integration_mdm_profiles_test.go
  • server/service/microsoft_mdm.go
💤 Files with no reviewable changes (1)
  • server/datastore/mysql/microsoft_mdm_test.go

Comment thread server/datastore/mysql/mdm_test.go
Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/datastore/mysql/microsoft_mdm.go
Lint fixes:
- Remove unused simulateWindowsRemoveReconciliation function
- Replace interface{} with any (modernize lint)

Integration test fixes:
- TestDeleteMDMProfileCancelsInstalls: both Windows hosts have
  non-NULL status (pending) after reconciler runs, so both get
  remove+pending (not just host4)
- TestWindowsProfileResend: account for <Delete> commands when
  profile content changes; clean up queued commands in subtest
  cleanup to prevent cross-contamination
- TestHostMDMProfilesExcludeLabels: add profile_name to
  windowsProfilesToRemoveQuery so remove rows have correct names
- Preserve NotFound error contract in DeleteMDMWindowsConfigProfile
  when profile doesn't exist (Copilot, CodeRabbit)
- Restore IsNotFound assertion in TestMDMWindowsConfigProfiles
- Only delete remove+verified rows in response handler cleanup, not
  verifying (Copilot: verifying is in-flight, not terminal)
- Restrict install query for remove rows to exclude verifying/verified
  to prevent install churn while Delete is in-flight (Copilot)
- Use 16-byte zero checksum instead of empty slice for BINARY(16)
  column compatibility (Copilot)
- Add deprecation comment to UpdateOrDeleteHostMDMWindowsProfile
  explaining it is superseded by response handler cleanup
- Remove SQL comment containing ? inside query template (was treated
  as bind variable by sqlx.In, causing runtime errors)
cleanupStaleWindowsRemoveRows now only queries and deletes remove
rows for hosts present in the current want map, instead of scanning
all remove rows in the table. This prevents implicitly hiding issues
for hosts the current test phase doesn't check.
- WindowsResponseToDeliveryStatusForRemove now treats 500 (Command
  Failed) as success. Windows returns 500 (not 405) for CSP nodes
  that do not support <Delete>, such as DeviceLock/AccountLockoutPolicy
  and some SystemServices nodes. Since removal is best-effort, this
  prevents permanent remove+failed rows in host details.
- TestDeleteMDMProfileCancelsInstalls: fix second Windows assertion
  at line 6927 to expect persistent remove+pending rows (no simulated
  device check-in processes the <Delete> command in this test).
- TestHostMDMProfilesExcludeLabels: fix assertion to expect install
  (not remove) after label exclusion is removed and profile becomes
  desired again -- the install query correctly flips remove rows back
  to install when the profile re-enters the desired state.
- TestWindowsProfileResend: content change updates profile in place
  (same name, different checksum) so only a re-install is sent, not
  a delete+install pair.
@getvictor

Copy link
Copy Markdown
Member Author

I think there's potentially one other miss here but it may be an accepted one.

The user story says:

so that I can make sure that if my organization decides to change company policies those are same for newly enrolled and already existing hosts.

If an admin using gitops edits an existing profile and tweaks the settings in it, perhaps removing some LocURIs, we won't delete those right? I'm not sure if this is a case of us needing to suggest different behaviors for admins or what but it feels like we're still not quite approaching the macOS behavior

@JordanMontgomery
Nice find. It doesn't work at all like Apple. I filed it as an unreleased bug: #42452
I'll make that fix (along with any fixes from load test) in a follow-up PR.
Can this one be approved/merged?

@JordanMontgomery JordanMontgomery 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.

@getvictor since there is a followup PR coming I think this is probably OK but I would try to test the following scenario:

  1. Admin registers system for MDM
  2. Admin uploads profile
  3. Profile synced to system
  4. System shutdown
  5. Admin deletes profile enqueuing deletes
  6. Admin uploads profile again enqueuing adds
  7. Device checks in - Do the DELETEs get added before or after the ADDs in the commands sent to the device?

It seems like we'd maybe want the commands sent to the device to be ordered. Because we send them all as one big blob today we don't do that but it didn't necessarily matter as much. Now it matters a lot more. I know you filed a bug around this but I think this new functionality perhaps makes this worse

…delete

# Conflicts:
#	server/datastore/mysql/microsoft_mdm.go
@getvictor

Copy link
Copy Markdown
Member Author

@getvictor since there is a followup PR coming I think this is probably OK but I would try to test the following scenario:

  1. Admin registers system for MDM
  2. Admin uploads profile
  3. Profile synced to system
  4. System shutdown
  5. Admin deletes profile enqueuing deletes
  6. Admin uploads profile again enqueuing adds
  7. Device checks in - Do the DELETEs get added before or after the ADDs in the commands sent to the device?

It seems like we'd maybe want the commands sent to the device to be ordered. Because we send them all as one big blob today we don't do that but it didn't necessarily matter as much. Now it matters a lot more. I know you filed a bug around this but I think this new functionality perhaps makes this worse

@JordanMontgomery Yes, this is an issue. I will fix it by ordering by created_at in the follow up. Also, this will not completely solve determinism since profiles created in a batch can have the same created_at.

@getvictor
getvictor merged commit 4e7c6f3 into main Mar 26, 2026
51 checks passed
@getvictor
getvictor deleted the victor/33418-windows-delete branch March 26, 2026 23:25
@coderabbitai coderabbitai Bot mentioned this pull request Jun 30, 2026
6 tasks
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.

Remove settings from Windows hosts when configuration profile is deleted

3 participants