Skip to content

Fix GitOps failure when moving labels from global to fleet scope (#44983) - #45003

Merged
nulmete merged 1 commit into
rc-minor-fleet-v4.85.0from
nulmete/fix-44950-gitops-null-label-id-cherry-pick
May 8, 2026
Merged

Fix GitOps failure when moving labels from global to fleet scope (#44983)#45003
nulmete merged 1 commit into
rc-minor-fleet-v4.85.0from
nulmete/fix-44950-gitops-null-label-id-cherry-pick

Conversation

@nulmete

@nulmete nulmete commented May 8, 2026

Copy link
Copy Markdown
Member

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 fleetctl gitops apply doesn't clear stale labels_exclude_any associations after label deletion #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

)

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
@nulmete
nulmete requested a review from a team as a code owner May 8, 2026 05:55

@claude claude Bot left a comment

Copy link
Copy Markdown

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.

@codecov

codecov Bot commented May 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.80%. Comparing base (9de8af6) to head (ef5eb6c).
⚠️ Report is 28 commits behind head on rc-minor-fleet-v4.85.0.

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     
Flag Coverage Δ
backend 68.58% <100.00%> (+0.08%) ⬆️

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.

@nulmete
nulmete merged commit 0d6e015 into rc-minor-fleet-v4.85.0 May 8, 2026
45 checks passed
@nulmete
nulmete deleted the nulmete/fix-44950-gitops-null-label-id-cherry-pick branch May 8, 2026 13:04
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.

3 participants