mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
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:
parent
968d33c0df
commit
27b6174543
4 changed files with 129 additions and 10 deletions
1
changes/26114-new-team-with-vpp-apps
Normal file
1
changes/26114-new-team-with-vpp-apps
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fixed `fleetctl gitops` issue where creating a new team containing VPP apps caused an error.
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
16
cmd/fleetctl/testdata/gitops/global_config_vpp.yml
vendored
Normal file
16
cmd/fleetctl/testdata/gitops/global_config_vpp.yml
vendored
Normal 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"
|
||||
Loading…
Reference in a new issue