GitOps support for scheduled auto updates settings (#37851)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #35457 # Checklist for submitter - [ ] 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. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ### `fleetctl generate-gitops` **UI** <img width="1866" height="787" alt="Screenshot 2026-01-07 at 1 14 45 PM" src="https://github.com/user-attachments/assets/ccb585ee-3074-4ebf-9aaf-6dbdb885cf89" /> **Generated YAML** <img width="694" height="354" alt="Screenshot 2026-01-07 at 1 15 23 PM" src="https://github.com/user-attachments/assets/3867b63d-265d-43bc-bd60-31fb14e83409" /> ### `fleetctl gitops` **Source YAML** I set the start_time to 18:30 and end_time to 19:30 for the first app <img width="359" height="354" alt="Screenshot 2026-01-07 at 1 18 33 PM" src="https://github.com/user-attachments/assets/4cde31e3-aa93-4b75-b296-cc101f507c2a" /> After the command ran, verified both on the UI and DB that the new values were set <img width="790" height="185" alt="Screenshot 2026-01-07 at 1 18 56 PM" src="https://github.com/user-attachments/assets/30d9806c-b26c-4f17-b505-facb7fd0df15" /> <img width="1432" height="708" alt="Screenshot 2026-01-07 at 1 19 15 PM" src="https://github.com/user-attachments/assets/5e123f94-ea69-4403-bba3-abf917cc1a01" /> ## New Fleet configuration settings - [ ] Setting(s) is/are explicitly excluded from GitOps If you didn't check the box above, follow this checklist for GitOps-enabled settings: - [x] Verified that the setting is exported via `fleetctl generate-gitops` - [x] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485): https://github.com/fleetdm/fleet/blob/a79ff22d84466106fccc3818d38f23b9d9dc62df/docs/Configuration/yaml-files.md?plain=1#L538-L540 - [x] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [ ] Verified that any relevant UI is disabled when GitOps mode is enabled ## fleetd/orbit/Fleet Desktop - [ ] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [ ] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [ ] Verified that fleetd runs on macOS, Linux and Windows - [ ] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))
This commit is contained in:
@@ -144,6 +144,14 @@ func (s *enterpriseIntegrationGitopsTestSuite) TearDownTest() {
|
||||
_, err := q.ExecContext(ctx, `DELETE FROM software_installers WHERE global_or_team_id = 0;`)
|
||||
return err
|
||||
})
|
||||
|
||||
vppTokens, err := s.DS.ListVPPTokens(ctx)
|
||||
require.NoError(t, err)
|
||||
for _, tok := range vppTokens {
|
||||
err := s.DS.DeleteVPPToken(ctx, tok.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
mysql.ExecAdhocSQL(t, s.DS, func(tx sqlx.ExtContext) error {
|
||||
_, err := tx.ExecContext(ctx, "DELETE FROM vpp_apps;")
|
||||
return err
|
||||
@@ -2711,3 +2719,212 @@ func labelTeamIDResult(t *testing.T, s *enterpriseIntegrationGitopsTestSuite, ct
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
// TestGitOpsVPPAppAutoUpdate tests that auto-update settings for VPP apps (iOS/iPadOS)
|
||||
// are properly applied via GitOps.
|
||||
func (s *enterpriseIntegrationGitopsTestSuite) TestGitOpsVPPAppAutoUpdate() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
user := s.createGitOpsUser(t)
|
||||
fleetctlConfig := s.createFleetctlConfig(t, user)
|
||||
|
||||
// Create a global VPP token (location is "Jungle")
|
||||
test.CreateInsertGlobalVPPToken(t, s.DS)
|
||||
|
||||
// Generate team name upfront since we need it in the global template
|
||||
teamName := uuid.NewString()
|
||||
|
||||
// The global template includes VPP token assignment to the team
|
||||
// The location "Jungle" comes from test.CreateInsertGlobalVPPToken
|
||||
globalTemplate := fmt.Sprintf(`
|
||||
agent_options:
|
||||
controls:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_URL
|
||||
org_info:
|
||||
org_name: Fleet
|
||||
secrets:
|
||||
mdm:
|
||||
volume_purchasing_program:
|
||||
- location: Jungle
|
||||
teams:
|
||||
- %s
|
||||
policies:
|
||||
queries:
|
||||
`, teamName)
|
||||
|
||||
teamTemplate := `
|
||||
controls:
|
||||
software:
|
||||
app_store_apps:
|
||||
- app_store_id: "2"
|
||||
platform: ios
|
||||
self_service: false
|
||||
auto_update_enabled: true
|
||||
auto_update_start_time: "02:00"
|
||||
auto_update_end_time: "06:00"
|
||||
- app_store_id: "2"
|
||||
platform: ipados
|
||||
self_service: false
|
||||
auto_update_enabled: true
|
||||
auto_update_start_time: "03:00"
|
||||
auto_update_end_time: "07:00"
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
name: %s
|
||||
team_settings:
|
||||
secrets: [{"secret":"enroll_secret"}]
|
||||
`
|
||||
|
||||
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||||
require.NoError(t, err)
|
||||
_, err = globalFile.WriteString(globalTemplate)
|
||||
require.NoError(t, err)
|
||||
err = globalFile.Close()
|
||||
require.NoError(t, err)
|
||||
teamFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||||
require.NoError(t, err)
|
||||
_, err = teamFile.WriteString(fmt.Sprintf(teamTemplate, teamName))
|
||||
require.NoError(t, err)
|
||||
err = teamFile.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Setenv("FLEET_URL", s.Server.URL)
|
||||
|
||||
testing_utils.StartAndServeVPPServer(t)
|
||||
|
||||
dryRunOutput := fleetctl.RunAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
|
||||
require.Contains(t, dryRunOutput, "gitops dry run succeeded")
|
||||
|
||||
realRunOutput := fleetctl.RunAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", teamFile.Name()})
|
||||
require.Contains(t, realRunOutput, "gitops succeeded")
|
||||
|
||||
team, err := s.DS.TeamByName(ctx, teamName)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify VPP apps were added
|
||||
titles, _, _, err := s.DS.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{AvailableForInstall: true, TeamID: &team.ID},
|
||||
fleet.TeamFilter{User: test.UserAdmin})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, titles, 2) // One for iOS, one for iPadOS
|
||||
|
||||
// Verify auto-update schedules were created in the database
|
||||
type autoUpdateSchedule struct {
|
||||
TitleID uint `db:"title_id"`
|
||||
TeamID uint `db:"team_id"`
|
||||
Enabled bool `db:"enabled"`
|
||||
StartTime string `db:"start_time"`
|
||||
EndTime string `db:"end_time"`
|
||||
}
|
||||
var schedules []autoUpdateSchedule
|
||||
mysql.ExecAdhocSQL(t, s.DS, func(q sqlx.ExtContext) error {
|
||||
return sqlx.SelectContext(ctx, q, &schedules,
|
||||
`SELECT title_id, team_id, enabled, start_time, end_time
|
||||
FROM software_update_schedules
|
||||
WHERE team_id = ?
|
||||
ORDER BY title_id`, team.ID)
|
||||
})
|
||||
|
||||
require.Len(t, schedules, 2)
|
||||
|
||||
for _, schedule := range schedules {
|
||||
require.Equal(t, team.ID, schedule.TeamID)
|
||||
require.True(t, schedule.Enabled)
|
||||
|
||||
var foundTitle *fleet.SoftwareTitleListResult
|
||||
for i := range titles {
|
||||
if titles[i].ID == schedule.TitleID {
|
||||
foundTitle = &titles[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, foundTitle, "should find title for schedule")
|
||||
|
||||
// Verify the correct start/end times based on source
|
||||
switch foundTitle.Source {
|
||||
case "ios_apps":
|
||||
require.Equal(t, "02:00", schedule.StartTime)
|
||||
require.Equal(t, "06:00", schedule.EndTime)
|
||||
case "ipados_apps":
|
||||
require.Equal(t, "03:00", schedule.StartTime)
|
||||
require.Equal(t, "07:00", schedule.EndTime)
|
||||
default:
|
||||
t.Fatalf("unexpected source: %s", foundTitle.Source)
|
||||
}
|
||||
}
|
||||
|
||||
// Now apply a config without auto-update fields for the iPadOS app and verify they're cleared
|
||||
teamTemplateNoAutoUpdate := `
|
||||
controls:
|
||||
software:
|
||||
app_store_apps:
|
||||
- app_store_id: "2"
|
||||
platform: ios
|
||||
self_service: false
|
||||
auto_update_enabled: true
|
||||
auto_update_start_time: "02:00"
|
||||
auto_update_end_time: "06:00"
|
||||
- app_store_id: "2"
|
||||
platform: ipados
|
||||
self_service: false
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
name: %s
|
||||
team_settings:
|
||||
secrets: [{"secret":"enroll_secret"}]
|
||||
`
|
||||
|
||||
teamFileNoAutoUpdate, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||||
require.NoError(t, err)
|
||||
_, err = teamFileNoAutoUpdate.WriteString(fmt.Sprintf(teamTemplateNoAutoUpdate, teamName))
|
||||
require.NoError(t, err)
|
||||
err = teamFileNoAutoUpdate.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Apply the updated config
|
||||
realRunOutput = fleetctl.RunAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", teamFileNoAutoUpdate.Name()})
|
||||
require.Contains(t, realRunOutput, "gitops succeeded")
|
||||
|
||||
// Verify auto-update schedules: iOS should still have settings, iPadOS should be disabled
|
||||
var updatedSchedules []autoUpdateSchedule
|
||||
mysql.ExecAdhocSQL(t, s.DS, func(q sqlx.ExtContext) error {
|
||||
return sqlx.SelectContext(ctx, q, &updatedSchedules,
|
||||
`SELECT title_id, team_id, enabled, start_time, end_time
|
||||
FROM software_update_schedules
|
||||
WHERE team_id = ?
|
||||
ORDER BY title_id`, team.ID)
|
||||
})
|
||||
|
||||
require.Len(t, updatedSchedules, 2)
|
||||
|
||||
for _, schedule := range updatedSchedules {
|
||||
var foundTitle *fleet.SoftwareTitleListResult
|
||||
for i := range titles {
|
||||
if titles[i].ID == schedule.TitleID {
|
||||
foundTitle = &titles[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, foundTitle, "should find title for schedule")
|
||||
|
||||
switch foundTitle.Source {
|
||||
case "ios_apps":
|
||||
// iOS app should still have auto-update enabled
|
||||
require.True(t, schedule.Enabled)
|
||||
require.Equal(t, "02:00", schedule.StartTime)
|
||||
require.Equal(t, "06:00", schedule.EndTime)
|
||||
case "ipados_apps":
|
||||
// iPadOS app should now have auto-update disabled (fields removed from config)
|
||||
// but the previous start/end times should still be preserved in the database
|
||||
require.False(t, schedule.Enabled)
|
||||
require.Equal(t, "03:00", schedule.StartTime)
|
||||
require.Equal(t, "07:00", schedule.EndTime)
|
||||
default:
|
||||
t.Fatalf("unexpected source: %s", foundTitle.Source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user