Fixed fleetctl gitops issue where creating a new team containing VPP apps caused an error. (#28624)

For #26114 

# Checklist for submitter
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Victor Lyuboslavsky 2025-04-29 16:28:25 -05:00 committed by GitHub
parent 968d33c0df
commit 27b6174543
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 129 additions and 10 deletions

View file

@ -0,0 +1 @@
Fixed `fleetctl gitops` issue where creating a new team containing VPP apps caused an error.

View file

@ -15,7 +15,10 @@ import (
"golang.org/x/text/unicode/norm"
)
const filenameMaxLength = 255
const (
filenameMaxLength = 255
reapplyingTeamForVPPAppsMsg = "[!] re-applying configs for team %s -- this only happens once for new teams that have VPP apps\n"
)
type LabelUsage struct {
Name string
@ -96,8 +99,14 @@ func gitopsCommand() *cli.Command {
var originalVPPConfig []any
var teamNames []string
var teamDryRunAssumptions *fleet.TeamSpecsDryRunAssumptions
var abmTeams, vppTeams []string
var hasMissingABMTeam, hasMissingVPPTeam, usesLegacyABMConfig bool
var abmTeams, vppTeams, missingVPPTeams []string
var hasMissingABMTeam, usesLegacyABMConfig bool
type missingVPPTeamWithApps struct {
config *spec.GitOps
vppApps []*fleet.TeamSpecAppStoreApp
filename string
}
var missingVPPTeamsWithApps []missingVPPTeamWithApps
// we keep track of team software installers and scripts for correct policy application
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
@ -233,7 +242,7 @@ func gitopsCommand() *cli.Command {
return err
}
vppTeams, hasMissingVPPTeam, err = checkVPPTeamAssignments(config, fleetClient)
vppTeams, missingVPPTeams, err = checkVPPTeamAssignments(config, fleetClient)
if err != nil {
return err
}
@ -258,7 +267,7 @@ func gitopsCommand() *cli.Command {
}
}
if hasMissingVPPTeam {
if len(missingVPPTeams) > 0 {
if mdm, ok := config.OrgSettings["mdm"]; ok {
if mdmMap, ok := mdm.(map[string]any); ok {
if vpp, ok := mdmMap["volume_purchasing_program"]; ok {
@ -275,6 +284,21 @@ 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.
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 flDryRun {
incomingSecrets := fleetClient.GetGitOpsSecrets(config)
for _, secret := range incomingSecrets {
@ -308,11 +332,23 @@ func gitopsCommand() *cli.Command {
return err
}
}
if len(vppTeams) > 0 && hasMissingVPPTeam {
if len(missingVPPTeams) > 0 {
if err = applyVPPTokenAssignmentIfNeeded(c, teamNames, vppTeams, originalVPPConfig, flDryRun, fleetClient); err != nil {
return err
}
}
// Now that VPP tokens have been assigned, we can apply VPP apps to the new team.
// For simplicity, we simply re-apply the entire config. This only happens once when the team is created.
for _, teamWithApps := range missingVPPTeamsWithApps {
_, _ = fmt.Fprintf(c.App.Writer, reapplyingTeamForVPPAppsMsg, *teamWithApps.config.TeamName)
teamWithApps.config.Software.AppStoreApps = teamWithApps.vppApps
_, err := fleetClient.DoGitOps(c.Context, teamWithApps.config, teamWithApps.filename, logf, flDryRun, teamDryRunAssumptions, appConfig,
teamsSoftwareInstallers, teamsVPPApps, teamsScripts)
if err != nil {
return err
}
}
if flDeleteOtherTeams && appConfig.License.IsPremium() { // skip team deletion for non-premium users
teams, err := fleetClient.ListTeams("")
if err != nil {
@ -621,13 +657,13 @@ func applyABMTokenAssignmentIfNeeded(
}
func checkVPPTeamAssignments(config *spec.GitOps, fleetClient *service.Client) (
vppTeams []string, missingTeam bool, err error,
vppTeams []string, missingTeams []string, err error,
) {
if mdm, ok := config.OrgSettings["mdm"]; ok {
if mdmMap, ok := mdm.(map[string]any); ok {
teams, err := fleetClient.ListTeams("")
if err != nil {
return nil, false, err
return nil, nil, err
}
teamNames := map[string]struct{}{}
for _, tm := range teams {
@ -645,7 +681,7 @@ func checkVPPTeamAssignments(config *spec.GitOps, fleetClient *service.Client) (
normalizedTeam := norm.NFC.String(teamStr)
vppTeams = append(vppTeams, normalizedTeam)
if _, ok := teamNames[normalizedTeam]; !ok {
missingTeam = true
missingTeams = append(missingTeams, normalizedTeam)
}
}
}
@ -657,7 +693,7 @@ func checkVPPTeamAssignments(config *spec.GitOps, fleetClient *service.Client) (
}
}
return vppTeams, missingTeam, nil
return vppTeams, missingTeams, nil
}
func applyVPPTokenAssignmentIfNeeded(

View file

@ -2,6 +2,7 @@ package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
@ -2785,6 +2786,60 @@ func TestGitOpsTeamVPPApps(t *testing.T) {
}
}
// TestGitOpsTeamVPPAndApp tests the flow where a new team is created with VPP apps.
// GitOps must first create the team, then assign VPP token to it, and only then add VPP apps.
func TestGitOpsTeamVPPAndApp(t *testing.T) {
startAndServeVPPServer(t)
ds, _, _ := setupFullGitOpsPremiumServer(t)
renewDate := time.Now().Add(24 * time.Hour)
token, err := test.CreateVPPTokenEncoded(renewDate, "fleet", "ca")
require.NoError(t, err)
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
}
// The following mocks are key to this test.
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
}
buf, err := runAppNoChecks([]string{"gitops", "-f", "testdata/gitops/global_config_vpp.yml", "-f", "testdata/gitops/team_vpp_valid_app.yml"})
require.NoError(t, err)
assert.True(t, ds.UpdateVPPTokenTeamsFuncInvoked)
assert.True(t, ds.GetVPPTokenByTeamIDFuncInvoked)
assert.True(t, ds.SetTeamVPPAppsFuncInvoked)
assert.Contains(t, buf.String(), fmt.Sprintf(reapplyingTeamForVPPAppsMsg, teamName))
}
func TestGitOpsCustomSettings(t *testing.T) {
cases := []struct {
file string
@ -3185,6 +3240,17 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
}
return nil, nil
}
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
summary := make([]*fleet.TeamSummary, 0, len(savedTeams))
for _, team := range savedTeams {
summary = append(summary, &fleet.TeamSummary{
ID: (*team).ID,
Name: (*team).Name,
Description: (*team).Description,
})
}
return summary, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}

View file

@ -0,0 +1,16 @@
policies:
queries:
agent_options:
controls:
org_settings:
mdm:
volume_purchasing_program:
- location: Earth
teams:
- "${TEST_TEAM_NAME}"
server_settings:
server_url: https://example.com
org_info:
org_name: Fleet
secrets:
- secret: "FLEET_GLOBAL_ENROLL_SECRET"