mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Defer all VPP apps when there are missing teams (#42862)
Fixes #40785 ## Summary When a GitOps run includes a `volume_purchasing_program` config that references a team that doesn't exist yet, the code temporarily removes the entire VPP config from the global AppConfig, clearing ALL VPP token-to-team assignments on the server. However, the code only deferred `app_store_apps` for the missing teams, not for existing teams that also lost their VPP assignments. Those existing teams then failed with "No available VPP Token" when their `app_store_apps` were applied. The fix widens the deferral scope to match the clearing scope. When VPP assignments are temporarily cleared, `app_store_apps` are now deferred for all teams in the VPP config, not just the missing ones.
This commit is contained in:
parent
e1f93cb28d
commit
aa0c0674a8
3 changed files with 155 additions and 12 deletions
1
changes/40785-fix-gitops-vpp-token-assignment
Normal file
1
changes/40785-fix-gitops-vpp-token-assignment
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fixed `fleetctl gitops` failing with "No available VPP Token" when assigning VPP apps alongside a new team.
|
||||
|
|
@ -19,7 +19,7 @@ import (
|
|||
|
||||
const (
|
||||
filenameMaxLength = 255
|
||||
ReapplyingTeamForVPPAppsMsg = "[!] re-applying configs for team %s -- this only happens once for new teams that have VPP apps\n"
|
||||
ReapplyingTeamForVPPAppsMsg = "[!] re-applying configs for team %s to set VPP apps\n"
|
||||
)
|
||||
|
||||
type LabelUsage struct {
|
||||
|
|
@ -518,18 +518,19 @@ func gitopsCommand() *cli.Command {
|
|||
}
|
||||
}
|
||||
|
||||
// We cannot apply a VPP app to a new team until that team gets a VPP token.
|
||||
// So, we create the team, then apply the VPP token, then apply VPP apps.
|
||||
// Teams need a VPP token before VPP apps can be applied. When some VPP
|
||||
// teams don't exist yet, the VPP config is temporarily removed from the
|
||||
// global config, which clears all VPP token assignments. To avoid
|
||||
// "No available VPP Token" errors, we defer app_store_apps for every
|
||||
// team in the VPP config and re-apply them after tokens are reassigned.
|
||||
if !isGlobalConfig && len(missingVPPTeams) > 0 && len(config.Software.AppStoreApps) > 0 {
|
||||
for _, missingTeam := range missingVPPTeams {
|
||||
if missingTeam == *config.TeamName {
|
||||
missingVPPTeamsWithApps = append(missingVPPTeamsWithApps, missingVPPTeamWithApps{
|
||||
config: config,
|
||||
vppApps: config.Software.AppStoreApps,
|
||||
filename: flFilename,
|
||||
})
|
||||
config.Software.AppStoreApps = nil
|
||||
}
|
||||
if slices.Contains(vppTeams, *config.TeamName) {
|
||||
missingVPPTeamsWithApps = append(missingVPPTeamsWithApps, missingVPPTeamWithApps{
|
||||
config: config,
|
||||
vppApps: config.Software.AppStoreApps,
|
||||
filename: flFilename,
|
||||
})
|
||||
config.Software.AppStoreApps = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -705,6 +705,147 @@ func TestGitOpsTeamVPPAndApp(t *testing.T) {
|
|||
assert.Contains(t, buf.String(), fmt.Sprintf(fleetctl.ReapplyingTeamForVPPAppsMsg, teamName))
|
||||
}
|
||||
|
||||
// TestGitOpsExistingTeamVPPAppsWithMissingTeam tests the scenario where:
|
||||
// - An existing team with app_store_apps is in the VPP config
|
||||
// - A NEW team (doesn't exist yet) is also in the VPP config
|
||||
// When there are missing VPP teams, the VPP config is temporarily cleared,
|
||||
// which removes VPP assignments for ALL teams. We must defer app_store_apps
|
||||
// for all VPP teams, not just missing ones. (Issue #40785)
|
||||
func TestGitOpsExistingTeamVPPAppsWithMissingTeam(t *testing.T) {
|
||||
testing_utils.StartAndServeVPPServer(t)
|
||||
ds, _, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)
|
||||
renewDate := time.Now().Add(24 * time.Hour)
|
||||
token, err := test.CreateVPPTokenEncoded(renewDate, "fleet", "ca")
|
||||
require.NoError(t, err)
|
||||
|
||||
existingTeamName := "Existing Team"
|
||||
newTeamName := "New Team"
|
||||
|
||||
// Pre-populate the existing team so checkVPPTeamAssignments sees it.
|
||||
existingTeam := &fleet.Team{ID: 42, Name: existingTeamName}
|
||||
savedTeams[existingTeamName] = &existingTeam
|
||||
|
||||
// No existing labels — this test doesn't test label behavior.
|
||||
ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
||||
return []fleet.VPPAppResponse{}, nil
|
||||
}
|
||||
ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
||||
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
||||
}
|
||||
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
vppToken := &fleet.VPPTokenDB{
|
||||
ID: 1,
|
||||
OrgName: "Fleet",
|
||||
Location: "Earth",
|
||||
RenewDate: renewDate,
|
||||
Token: string(token),
|
||||
Teams: nil,
|
||||
}
|
||||
tokensByTeams := make(map[uint]*fleet.VPPTokenDB)
|
||||
ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
|
||||
for _, teamID := range teams {
|
||||
tokensByTeams[teamID] = vppToken
|
||||
}
|
||||
return vppToken, nil
|
||||
}
|
||||
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
||||
return []*fleet.VPPTokenDB{vppToken}, nil
|
||||
}
|
||||
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
||||
if teamID == nil {
|
||||
return vppToken, nil
|
||||
}
|
||||
token, ok := tokensByTeams[*teamID]
|
||||
if !ok {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
||||
return []uint{}, nil
|
||||
}
|
||||
ds.InsertOrReplaceMDMConfigAssetFunc = func(ctx context.Context, asset fleet.MDMConfigAsset) error {
|
||||
return nil
|
||||
}
|
||||
ds.HardDeleteMDMConfigAssetFunc = func(ctx context.Context, assetName fleet.MDMAssetName) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
|
||||
return &fleet.TeamLite{}, nil
|
||||
}
|
||||
|
||||
globalCfg := fmt.Sprintf(`
|
||||
policies:
|
||||
queries:
|
||||
agent_options:
|
||||
controls:
|
||||
org_settings:
|
||||
mdm:
|
||||
volume_purchasing_program:
|
||||
- location: Earth
|
||||
teams:
|
||||
- %q
|
||||
- %q
|
||||
server_settings:
|
||||
server_url: https://example.com
|
||||
org_info:
|
||||
org_name: Fleet
|
||||
secrets:
|
||||
- secret: "FLEET_GLOBAL_ENROLL_SECRET"
|
||||
`, existingTeamName, newTeamName)
|
||||
|
||||
teamCfg := func(name string) string {
|
||||
return fmt.Sprintf(`
|
||||
name: %q
|
||||
team_settings:
|
||||
secrets:
|
||||
- secret: "%s-secret"
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
host_expiry_settings:
|
||||
host_expiry_enabled: true
|
||||
host_expiry_window: 30
|
||||
agent_options:
|
||||
controls:
|
||||
policies:
|
||||
queries:
|
||||
software:
|
||||
app_store_apps:
|
||||
- app_store_id: "1"
|
||||
`, name, name)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
globalFile := filepath.Join(tmpDir, "default.yml")
|
||||
require.NoError(t, os.WriteFile(globalFile, []byte(globalCfg), 0o644))
|
||||
existingTeamFile := filepath.Join(tmpDir, "existing-team.yml")
|
||||
require.NoError(t, os.WriteFile(existingTeamFile, []byte(teamCfg(existingTeamName)), 0o644))
|
||||
newTeamFile := filepath.Join(tmpDir, "new-team.yml")
|
||||
require.NoError(t, os.WriteFile(newTeamFile, []byte(teamCfg(newTeamName)), 0o644))
|
||||
|
||||
buf, err := fleetctl.RunAppNoChecks([]string{
|
||||
"gitops", "-f", globalFile, "-f", existingTeamFile, "-f", newTeamFile,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ds.UpdateVPPTokenTeamsFuncInvoked)
|
||||
assert.True(t, ds.GetVPPTokenByTeamIDFuncInvoked)
|
||||
assert.True(t, ds.SetTeamVPPAppsFuncInvoked)
|
||||
// Both teams should have had their VPP apps deferred and re-applied.
|
||||
assert.Contains(t, buf.String(), fmt.Sprintf(fleetctl.ReapplyingTeamForVPPAppsMsg, existingTeamName))
|
||||
assert.Contains(t, buf.String(), fmt.Sprintf(fleetctl.ReapplyingTeamForVPPAppsMsg, newTeamName))
|
||||
}
|
||||
|
||||
func TestGitOpsVPP(t *testing.T) {
|
||||
global := func(mdm string) string {
|
||||
return fmt.Sprintf(`
|
||||
|
|
|
|||
Loading…
Reference in a new issue