Fix macOS "Update new hosts to latest" staying enabled in GitOps after clearing version/deadline (#45984) (#47602)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #45984 

Fix is applied on the GitOps side since that's what I figured the
customer was using on the [Slack
thread](https://fleetdm.slack.com/archives/C061ZA91Y1J/p1779372669701129).

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

#### Before

Reproduced on `main`:
- Set `update_new_hosts: true` beforehand.
- Ran `gitops` with `update_new_hosts` commented out. It was still kept
as `true`.



https://github.com/user-attachments/assets/f6b41f0d-38e6-468f-a605-b3e66b7b2dbc

#### After

Running `gitops` with `update_new_hosts` commented out switched its
value to `false`.



https://github.com/user-attachments/assets/24756063-b3a9-400b-a2cc-208dd816a556



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Corrected GitOps behavior for the macOS “Update new hosts to latest”
setting so it no longer stays enabled after clearing `minimum_version`
and `deadline`; it now defaults to disabled unless both are set.

* **Tests**
* Added GitOps test coverage to verify the defaulting outcomes across
YAML variations for the macOS update settings, including explicit and
empty field combinations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Nico
2026-06-16 10:21:20 -03:00
committed by GitHub
parent 76de4adfcb
commit 12d2aba40c
3 changed files with 102 additions and 4 deletions
@@ -0,0 +1 @@
* Fixed an issue where the macOS "Update new hosts to latest" OS update setting could stay enabled in GitOps after `minimum_version` and `deadline` were cleared; when `update_new_hosts` isn't explicitly set, it now defaults to enabled only while a minimum version and deadline are configured.
+94
View File
@@ -570,6 +570,100 @@ software:
// assert.Equal(t, "hydrant2_secret", h2.ClientSecret)
}
func TestGitOpsMacOSUpdateNewHostsDefault(t *testing.T) {
cases := []struct {
name string
currentlyStoredUpdateNewHosts optjson.Bool
macOSUpdatesYAML string
wantUpdateNewHosts optjson.Bool
}{
{
name: "empty minimum_version and deadline default update_new_hosts to false",
currentlyStoredUpdateNewHosts: optjson.SetBool(true),
macOSUpdatesYAML: `
macos_updates:
deadline: ""
minimum_version: ""`,
wantUpdateNewHosts: optjson.SetBool(false),
},
{
name: "configured minimum_version and deadline default update_new_hosts to true",
currentlyStoredUpdateNewHosts: optjson.SetBool(false),
macOSUpdatesYAML: `
macos_updates:
deadline: "2024-03-03"
minimum_version: "14.6.1"`,
wantUpdateNewHosts: optjson.SetBool(true),
},
{
// "Update all new hosts to latest" with no minimum version is a valid config, so an
// explicit update_new_hosts: true must be honored even when version/deadline are empty.
name: "explicit update_new_hosts true is honored without minimum_version or deadline",
currentlyStoredUpdateNewHosts: optjson.SetBool(false),
macOSUpdatesYAML: `
macos_updates:
deadline: ""
minimum_version: ""
update_new_hosts: true`,
wantUpdateNewHosts: optjson.SetBool(true),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{
License: license,
KeyValueStore: testing_utils.NewMemKeyValueStore(),
})
// Mock Apple GDMF API (required for validating OS update minimum version settings).
mdmtest.StartNewAppleGDMFTestServer(t)
setupEmptyGitOpsMocks(ds)
storedUpdateNewHosts := c.currentlyStoredUpdateNewHosts
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
cfg := &fleet.AppConfig{}
cfg.MDM.MacOSUpdates.UpdateNewHosts = storedUpdateNewHosts
return cfg, nil
}
savedAppConfig := &fleet.AppConfig{}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
ds.HasAppleUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
t.Setenv("FLEET_SERVER_URL", "https://fleet.example.com")
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = tmpFile.WriteString(`
controls:` + c.macOSUpdatesYAML + `
queries:
policies:
labels:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_name: GitOps Test
secrets:
software:
`)
require.NoError(t, err)
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
require.Equal(t, c.wantUpdateNewHosts, savedAppConfig.MDM.MacOSUpdates.UpdateNewHosts)
})
}
}
// TestGitOpsWindowsEntraIDs verifies that Windows Entra tenant and application client IDs round-trip through a
// `fleetctl gitops` apply (issue #46388). The client IDs include an upper-case GUID and a case-only duplicate of it,
// which exercises server-side normalization: client IDs are authorized case-insensitively, so they are stored
+7 -4
View File
@@ -2388,10 +2388,13 @@ func (c *Client) DoGitOps(
macOSUpdates["deadline"] = ""
}
// To keep things backward compatible, if a minimum_version and deadline are both set but the user hasn't set update_new_hosts,
// then we default update_new_hosts to true
if macOSUpdates["minimum_version"] != "" && macOSUpdates["deadline"] != "" && macOSUpdates["update_new_hosts"] == nil {
macOSUpdates["update_new_hosts"] = true
// When update_new_hosts isn't explicitly set, derive it from whether OS updates
// are configured: default to true when both minimum_version and deadline are set
// (kept for backward compatibility) and false otherwise. Defaulting to false when
// updates aren't configured prevents a previously stored "true" from sticking
// around once minimum_version/deadline are cleared.
if macOSUpdates["update_new_hosts"] == nil {
macOSUpdates["update_new_hosts"] = macOSUpdates["minimum_version"] != "" && macOSUpdates["deadline"] != ""
}
// Put in default values for ios_updates