Fix GitOps failure when moving labels from global to fleet scope (#44983) - #45003
Merged
nulmete merged 1 commit intoMay 8, 2026
Merged
Conversation
) Closes #44950 Reproduced locally using a MySQL integration test against the local test database. The test simulates the exact GitOps scenario from the issue: 1. Create a label and associate it with an MDM profile 2. Delete the label (FK `ON DELETE SET NULL` sets `label_id = NULL`) 3. Create a new label with the **same name** (simulates moving from global to fleet scope) 4. Call `batchSetProfileLabelAssociationsDB` with the profile referencing the new label **Before fix** (code from `main`, unfixed): ``` $ MYSQL_TEST=1 go test -run "TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated" -v -count=1 ./server/datastore/mysql/... === RUN TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated_after_deletion_darwin Error: selecting existing profile labels: sql: Scan error on column index 1, name "label_id": converting NULL to uint is unsupported === RUN TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated_after_deletion_windows Error: selecting existing profile labels: sql: Scan error on column index 1, name "label_id": converting NULL to uint is unsupported --- FAIL: TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated_after_deletion_darwin (0.02s) --- FAIL: TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated_after_deletion_windows (0.02s) FAIL ``` **After fix:** ``` $ MYSQL_TEST=1 go test -run "TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated" -v -count=1 ./server/datastore/mysql/... === RUN TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated_after_deletion_windows === RUN TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated_after_deletion_darwin --- PASS: TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated_after_deletion_windows (0.03s) --- PASS: TestMDMShared/TestBatchSetProfileLabelAssociations/same_label_name_recreated_after_deletion_darwin (0.03s) PASS ok github.com/fleetdm/fleet/v4/server/datastore/mysql 2.761s ``` When a label is deleted, MySQL's `ON DELETE SET NULL` foreign key constraint automatically sets `label_id = NULL` in the profile-label association row. The Go code then crashes trying to scan that NULL into a `uint` field. - **`server/datastore/mysql/mdm.go`** — Added `COALESCE(label_id, 0)` to the SELECT in `batchSetProfileLabelAssociationsDB`, so that NULL `label_id` values are returned as 0 instead of causing a scan error when Go tries to read NULL into a `uint`. - **`server/datastore/mysql/apple_mdm.go`** — Same `COALESCE(label_id, 0)` fix in `batchSetDeclarationLabelAssociationsDB`. Also added `OR label_id IS NULL` to the DELETE statement to clean up broken rows, matching the profile labels behavior from #42637. Other queries in the same codebase (e.g., `listProfileLabelsForProfiles`) already use `COALESCE(label_id, 0)` — these two were missed. - `same_label_name_recreated_after_deletion_{darwin,windows}` — reproduces the exact bug: associates a profile with a label, deletes the label (NULL label_id), creates a new label with the same name, and verifies `batchSetProfileLabelAssociationsDB` succeeds, the broken row is cleaned up, and the correct label association exists - Full MDM test suite passes: `MYSQL_TEST=1 go test -run "TestMDM" ./server/datastore/mysql/...` (76s) - `make lint-go-incremental` passes
There was a problem hiding this comment.
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.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## rc-minor-fleet-v4.85.0 #45003 +/- ##
==========================================================
+ Coverage 66.72% 66.80% +0.07%
==========================================================
Files 2626 2630 +4
Lines 211198 211835 +637
Branches 9428 9418 -10
==========================================================
+ Hits 140924 141507 +583
Misses 57485 57485
- Partials 12789 12843 +54
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
juan-fdz-hawa
approved these changes
May 8, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #44950
Reproduced locally using a MySQL integration test against the local test database. The test simulates the exact GitOps scenario from the issue:
ON DELETE SET NULLsetslabel_id = NULL)batchSetProfileLabelAssociationsDBwith the profile referencing the new labelBefore fix (code from
main, unfixed):After fix:
When a label is deleted, MySQL's
ON DELETE SET NULLforeign key constraint automatically setslabel_id = NULLin the profile-label association row. The Go code then crashes trying to scan that NULL into auintfield.server/datastore/mysql/mdm.go— AddedCOALESCE(label_id, 0)to the SELECT inbatchSetProfileLabelAssociationsDB, so that NULLlabel_idvalues are returned as 0 instead of causing a scan error when Go tries to read NULL into auint.server/datastore/mysql/apple_mdm.go— SameCOALESCE(label_id, 0)fix inbatchSetDeclarationLabelAssociationsDB. Also addedOR label_id IS NULLto the DELETE statement to clean up broken rows, matching the profile labels behavior fromfleetctl gitops applydoesn't clear stalelabels_exclude_anyassociations after label deletion #42637.Other queries in the same codebase (e.g.,
listProfileLabelsForProfiles) already useCOALESCE(label_id, 0)— these two were missed.same_label_name_recreated_after_deletion_{darwin,windows}— reproduces the exact bug: associates a profile with a label, deletes the label (NULL label_id), creates a new label with the same name, and verifiesbatchSetProfileLabelAssociationsDBsucceeds, the broken row is cleaned up, and the correct label association existsMYSQL_TEST=1 go test -run "TestMDM" ./server/datastore/mysql/...(76s)make lint-go-incrementalpasses