Fix fleetctl generate-gitops failing to include VPP fleet assignments (#42429)

This commit is contained in:
Luke Heath 2026-03-26 19:06:51 -05:00 committed by GitHub
parent 4e7c6f33a7
commit 26d0dccc8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 186 additions and 14 deletions

View file

@ -0,0 +1 @@
- Fixed `fleetctl generate-gitops` failing to include VPP fleet assignments.

View file

@ -89,6 +89,7 @@ type generateGitopsClient interface {
GetCertificateAuthoritiesSpec(includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error)
GetCertificateTemplates(teamID string) ([]*fleet.CertificateTemplateResponseSummary, error)
GetFleetMaintainedApp(id uint) (*fleet.MaintainedApp, error)
GetVPPTokens() ([]*fleet.VPPTokenDB, error)
ListFleetMaintainedApps(teamID uint) ([]fleet.MaintainedApp, error)
}
@ -1107,7 +1108,23 @@ func (cmd *GenerateGitopsCommand) generateMDM(mdm *fleet.MDM) (map[string]interf
}
if cmd.AppConfig.License.IsPremium() {
result[jsonFieldName(t, "AppleBusinessManager")] = mdm.AppleBusinessManager
result[jsonFieldName(t, "VolumePurchasingProgram")] = mdm.VolumePurchasingProgram
vppTokens, err := cmd.Client.GetVPPTokens()
if err != nil {
fmt.Fprintf(cmd.CLI.App.ErrWriter, "Error fetching VPP tokens: %s\n", err)
return nil, err
}
var vppConfig []fleet.MDMAppleVolumePurchasingProgramInfo
for _, token := range vppTokens {
teamNames := make([]string, 0, len(token.Teams))
for _, team := range token.Teams {
teamNames = append(teamNames, team.Name)
}
vppConfig = append(vppConfig, fleet.MDMAppleVolumePurchasingProgramInfo{
Location: token.Location,
Teams: teamNames,
})
}
result[jsonFieldName(t, "VolumePurchasingProgram")] = vppConfig
var eulaPath string
if cmd.AppConfig.MDM.EnabledAndConfigured {

View file

@ -31,6 +31,7 @@ type MockClient struct {
IsFree bool
TeamNameOverride string
WithoutMDM bool
WithoutVPP bool
}
func (c *MockClient) GetAppConfig() (*fleet.EnrichedAppConfig, error) {
@ -822,6 +823,24 @@ func (MockClient) GetCertificateTemplates(teamID string) ([]*fleet.CertificateTe
return res, nil
}
func (c *MockClient) GetVPPTokens() ([]*fleet.VPPTokenDB, error) {
if c.WithoutVPP {
return nil, nil
}
return []*fleet.VPPTokenDB{
{
ID: 1,
Location: "Fleet Device Management Inc.",
Teams: []fleet.TeamTuple{
{ID: 1, Name: "💻 Workstations"},
{ID: 2, Name: "💻🐣 Workstations (canary)"},
{ID: 3, Name: "📱🏢 Company-owned mobile devices"},
{ID: 4, Name: "📱🔐 Personal mobile devices"},
},
},
}, nil
}
func maskSecret(value string, shouldShowSecret bool) string {
if shouldShowSecret {
return value
@ -1963,7 +1982,7 @@ func verifyControlsHasMacosSetup(t *testing.T, controlsRaw map[string]interface{
func TestGenerateControlsAndMDMWithoutMDMEnabledAndConfigured(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
fleetClient := &MockClient{WithoutVPP: true}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
appConfig.MDM.EnabledAndConfigured = false
@ -1971,7 +1990,6 @@ func TestGenerateControlsAndMDMWithoutMDMEnabledAndConfigured(t *testing.T) {
appConfig.MDM.AppleBusinessManager = optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{}
appConfig.MDM.AppleServerURL = ""
appConfig.MDM.EndUserAuthentication = fleet.MDMEndUserAuthentication{}
appConfig.MDM.VolumePurchasingProgram = optjson.Slice[fleet.MDMAppleVolumePurchasingProgramInfo]{}
// Create the command.
cmd := &GenerateGitopsCommand{
@ -2006,6 +2024,144 @@ func TestGenerateControlsAndMDMWithoutMDMEnabledAndConfigured(t *testing.T) {
}
}
func TestGenerateMDMVPPTokens(t *testing.T) {
tests := []struct {
name string
vppTokens []*fleet.VPPTokenDB
expected []fleet.MDMAppleVolumePurchasingProgramInfo
}{
{
name: "no VPP tokens",
vppTokens: nil,
expected: nil,
},
{
name: "single token with teams",
vppTokens: []*fleet.VPPTokenDB{
{
ID: 1,
Location: "Acme Inc.",
Teams: []fleet.TeamTuple{
{ID: 1, Name: "Workstations"},
{ID: 2, Name: "Servers"},
},
},
},
expected: []fleet.MDMAppleVolumePurchasingProgramInfo{
{Location: "Acme Inc.", Teams: []string{"Workstations", "Servers"}},
},
},
{
name: "multiple tokens with different teams",
vppTokens: []*fleet.VPPTokenDB{
{
ID: 1,
Location: "Acme Inc.",
Teams: []fleet.TeamTuple{
{ID: 1, Name: "Workstations"},
},
},
{
ID: 2,
Location: "Widgets LLC",
Teams: []fleet.TeamTuple{
{ID: 2, Name: "Servers"},
{ID: 0, Name: fleet.TeamNameNoTeam},
},
},
},
expected: []fleet.MDMAppleVolumePurchasingProgramInfo{
{Location: "Acme Inc.", Teams: []string{"Workstations"}},
{Location: "Widgets LLC", Teams: []string{"Servers", fleet.TeamNameNoTeam}},
},
},
{
name: "token assigned to all teams (empty teams slice)",
vppTokens: []*fleet.VPPTokenDB{
{
ID: 1,
Location: "Acme Inc.",
Teams: []fleet.TeamTuple{},
},
},
expected: []fleet.MDMAppleVolumePurchasingProgramInfo{
{Location: "Acme Inc.", Teams: []string{}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fleetClient := &MockClient{WithoutVPP: true}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Use a wrapper to override the VPP tokens for this test case.
wrapper := &vppMockClientWrapper{
MockClient: fleetClient,
vppTokens: tt.vppTokens,
}
cmd := &GenerateGitopsCommand{
Client: wrapper,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]any),
AppConfig: appConfig,
ScriptList: make(map[uint]string),
}
mdmRaw, err := cmd.generateMDM(&appConfig.MDM)
require.NoError(t, err)
require.Contains(t, mdmRaw, "volume_purchasing_program")
vppRaw := mdmRaw["volume_purchasing_program"]
if tt.expected == nil {
require.Nil(t, vppRaw)
} else {
vppResult, ok := vppRaw.([]fleet.MDMAppleVolumePurchasingProgramInfo)
require.True(t, ok)
require.Equal(t, tt.expected, vppResult)
}
})
}
t.Run("GetVPPTokens error", func(t *testing.T) {
fleetClient := &MockClient{WithoutVPP: true}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
wrapper := &vppMockClientWrapper{
MockClient: fleetClient,
vppErr: errors.New("vpp tokens unavailable"),
}
cmd := &GenerateGitopsCommand{
Client: wrapper,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]any),
AppConfig: appConfig,
ScriptList: make(map[uint]string),
}
mdmRaw, err := cmd.generateMDM(&appConfig.MDM)
require.Error(t, err)
require.Nil(t, mdmRaw)
})
}
// vppMockClientWrapper wraps MockClient but overrides GetVPPTokens.
type vppMockClientWrapper struct {
*MockClient
vppTokens []*fleet.VPPTokenDB
vppErr error
}
func (w *vppMockClientWrapper) GetVPPTokens() ([]*fleet.VPPTokenDB, error) {
return w.vppTokens, w.vppErr
}
func TestSillyTeamNames(t *testing.T) {
configureFMAManifestServer(t)
sillyTeamNames := map[string]string{

View file

@ -267,17 +267,7 @@
"windows_settings": {
"custom_settings": []
},
"volume_purchasing_program": [
{
"location": "Fleet Device Management Inc.",
"teams": [
"💻 Workstations",
"💻🐣 Workstations (canary)",
"📱🏢 Company-owned mobile devices",
"📱🔐 Personal mobile devices"
]
}
],
"volume_purchasing_program": null,
"android_enabled_and_configured": true,
"android_settings": {
"custom_settings": []

View file

@ -40,6 +40,14 @@ func (c *Client) GetAppleBM() (*fleet.AppleBM, error) {
return responseBody.AppleBM, err
}
// GetVPPTokens retrieves the List Volume Purchasing Program (VPP) tokens
func (c *Client) GetVPPTokens() ([]*fleet.VPPTokenDB, error) {
verb, path := "GET", "/api/latest/fleet/vpp_tokens"
var responseBody getVPPTokensResponse
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, "")
return responseBody.Tokens, err
}
func (c *Client) CountABMTokens() (int, error) {
verb, path := "GET", "/api/latest/fleet/abm_tokens/count"
var responseBody countABMTokensResponse