Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
01efc8a
Initial WIP for using App Store Connect endpoint (or workalike) to gr…
iansltx Jan 4, 2026
9d8ca68
Switch other VPP metadata pull usages to new method
iansltx Jan 5, 2026
6df5619
Fix issues with app retrieval + refreshes
iansltx Jan 6, 2026
d2debd6
Implement VPP metadata proxy client interactions
iansltx Jan 7, 2026
68a8170
Pull server info from the right place, fix licensing, start fixing re…
iansltx Jan 7, 2026
4b2ea3f
Fix retry logic, clear out old access tokens in MDM assets rather tha…
iansltx Jan 7, 2026
126149e
Fix lint
iansltx Jan 7, 2026
01dc37c
Add changes file
iansltx Jan 7, 2026
099b2fa
Make mock
iansltx Jan 7, 2026
76e3fe2
Tell gosec to calm down
iansltx Jan 7, 2026
6119657
🤖 Fix some tests
iansltx Jan 7, 2026
80d107c
Clean up tests, validate (stubbed) proxy auth calls as well as calls …
iansltx Jan 7, 2026
c8dbf60
More test fixes
iansltx Jan 8, 2026
f38592f
🤖 Add test coverage to VPP API proxy
iansltx Jan 8, 2026
4cb42cc
Clean up/extend tests
iansltx Jan 8, 2026
43d0093
More test fixes, tweak VPP refresh to ensure we're mapping platform-s…
iansltx Jan 8, 2026
3cecd29
No longer TODOs
iansltx Jan 8, 2026
666ccd7
Merge branch 'main' into 32461-custom-vpp-apps
iansltx Jan 8, 2026
c3485e9
🤖 Add 429 handling test
iansltx Jan 8, 2026
1425443
Tweak retry limit to make sure we keep retrying on 429s
iansltx Jan 8, 2026
9eac176
Make it easier to specify a custom storefront for VPP apps
iansltx Jan 8, 2026
2b2d4fc
Make sure wantErr is tested, fix fake server to use injected handler …
iansltx Jan 8, 2026
9868721
Describe Authenticator type
iansltx Jan 8, 2026
e0ce481
Fix lint
iansltx Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changes/32461-custom-vpp-apps
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Added custom VPP apps to available VPP apps listing
* Fixed cases where Fleet would show the wrong current VPP app version when app versions varied by platform
4 changes: 3 additions & 1 deletion cmd/fleet/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/android"
android_svc "github.com/fleetdm/fleet/v4/server/mdm/android/service"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/apple_apps"
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
"github.com/fleetdm/fleet/v4/server/mdm/assets"
maintained_apps "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps"
Expand Down Expand Up @@ -1753,6 +1754,7 @@ func newRefreshVPPAppVersionsSchedule(
instanceID string,
ds fleet.Datastore,
logger kitlog.Logger,
vppAuth apple_apps.Authenticator,
) (*schedule.Schedule, error) {
const (
name = string(fleet.CronRefreshVPPAppVersions)
Expand All @@ -1764,7 +1766,7 @@ func newRefreshVPPAppVersionsSchedule(
ctx, name, instanceID, defaultInterval, ds, ds,
schedule.WithLogger(logger),
schedule.WithJob("refresh_vpp_app_version", func(ctx context.Context) error {
return vpp.RefreshVersions(ctx, ds)
return vpp.RefreshVersions(ctx, ds, vppAuth)
}),
)

Expand Down
7 changes: 4 additions & 3 deletions cmd/fleet/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mail"
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/apple_apps"
"github.com/fleetdm/fleet/v4/server/mdm/cryptoutil"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
Expand Down Expand Up @@ -131,7 +132,7 @@ the way that the Fleet server works.
applyDevFlags(&config)
}

license, err := initLicense(config, devLicense, devExpiredLicense)
license, err := initLicense(&config, devLicense, devExpiredLicense)
if err != nil {
initFatal(
err,
Expand Down Expand Up @@ -1164,7 +1165,7 @@ the way that the Fleet server works.
}

if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
return newRefreshVPPAppVersionsSchedule(ctx, instanceID, ds, logger)
return newRefreshVPPAppVersionsSchedule(ctx, instanceID, ds, logger, apple_apps.GetAuthenticator(ctx, ds, config.License.Key))
}); err != nil {
initFatal(err, "failed to register refresh vpp app versions schedule")
}
Expand Down Expand Up @@ -1691,7 +1692,7 @@ func printFleetv4732FixNeededMessage() {
"################################################################################\n", os.Args[0])
}

func initLicense(config configpkg.FleetConfig, devLicense, devExpiredLicense bool) (*fleet.LicenseInfo, error) {
func initLicense(config *configpkg.FleetConfig, devLicense, devExpiredLicense bool) (*fleet.LicenseInfo, error) {
if devLicense {
// This license key is valid for development only
config.License.Key = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCBJbmMuIiwiZXhwIjoxNzgyNzc3NjAwLCJzdWIiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCwgSW5jLiBEZXZlbG9wZXIiLCJkZXZpY2VzIjoxMDAwLCJub3RlIjoiQ3JlYXRlZCB3aXRoIEZsZWV0IExpY2Vuc2Uga2V5IGRpc3BlbnNlciIsInRpZXIiOiJwcmVtaXVtIiwiaWF0IjoxNzY3MjAzODg2fQ.X9O3CXJOzIfgkzlXgL45iBaSvAbZyQn4UjcvH_gEXJGIQw0xMW4r3tJBSEuUqQXoaQnADVR1Oocfp6j_hMZX0A"
Expand Down
2 changes: 1 addition & 1 deletion cmd/fleet/vuln_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ by an exit code of zero.`,
logger := initLogger(cfg)
logger = kitlog.With(logger, fleet.CronVulnerabilities)

licenseInfo, err := initLicense(cfg, devLicense, devExpiredLicense)
licenseInfo, err := initLicense(&cfg, devLicense, devExpiredLicense)
if err != nil {
return err
}
Expand Down
55 changes: 7 additions & 48 deletions cmd/fleetctl/fleetctl/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"slices"
Expand All @@ -24,7 +22,6 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
"github.com/fleetdm/fleet/v4/server/mock"
Expand Down Expand Up @@ -1535,50 +1532,6 @@ software:
assert.Equal(t, filepath.Base(tmpFile.Name()), *savedTeam.Filename)
}

func createFakeITunesAndVPPServices(t *testing.T) {
config := &testing_utils.AppleVPPConfigSrvConf{
Assets: []vpp.Asset{
{
AdamID: "1",
PricingParam: "STDQ",
AvailableCount: 12,
},
{
AdamID: "2",
PricingParam: "STDQ",
AvailableCount: 3,
},
},
SerialNumbers: []string{"123", "456"},
}
testing_utils.StartVPPApplyServer(t, config)

appleITunesSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// a map of apps we can respond with
db := map[string]string{
// macos app
"1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
// macos, ios, ipados app
"2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2,
"supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`,
// ipados app
"3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3,
"supportedDevices": ["iPadAir-iPadAir"] }`,
}

adamIDString := r.URL.Query().Get("id")
adamIDs := strings.Split(adamIDString, ",")

var objs []string
for _, a := range adamIDs {
objs = append(objs, db[a])
}

_, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ","))))
}))
t.Setenv("FLEET_DEV_ITUNES_URL", appleITunesSrv.URL)
}

func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
Expand Down Expand Up @@ -1820,8 +1773,14 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
return nil, nil
}
ds.HardDeleteMDMConfigAssetFunc = func(ctx context.Context, assetName fleet.MDMAssetName) error {
return nil
}
ds.InsertOrReplaceMDMConfigAssetFunc = func(ctx context.Context, asset fleet.MDMConfigAsset) error {
return nil
}

createFakeITunesAndVPPServices(t)
testing_utils.StartAndServeVPPServer(t)

globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
Expand Down
82 changes: 71 additions & 11 deletions cmd/fleetctl/fleetctl/testing_utils/testing_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,30 +662,90 @@ func StartAndServeVPPServer(t *testing.T) {

StartVPPApplyServer(t, config)

appleITunesSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// a map of apps we can respond with
// Set up the VPP proxy metadata server using the new format
// This replaces the old iTunes API format
vppProxySrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer test-bearer-token" {
w.WriteHeader(401)
_, _ = w.Write([]byte(`{"error": "unauthorized"}`))
return
}

// deviceFamilies: "mac" -> osx platform, "iphone" -> ios platform, "ipad" -> ios platform
db := map[string]string{
// macos app
"1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
"1": `{
"id": "1",
"attributes": {
"name": "App 1",
"platformAttributes": {
"osx": {
"bundleId": "a-1",
"artwork": {"url": "https://example.com/images/1/{w}x{h}.{f}"},
"latestVersionInfo": {"versionDisplay": "1.0.0"}
}
},
"deviceFamilies": ["mac"]
}
}`,
// macos, ios, ipados app
"2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2,
"supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`,
"2": `{
"id": "2",
"attributes": {
"name": "App 2",
"platformAttributes": {
"osx": {
"bundleId": "b-2",
"artwork": {"url": "https://example.com/images/2-mac/{w}x{h}.{f}"},
"latestVersionInfo": {"versionDisplay": "1.2.3"}
},
"ios": {
"bundleId": "b-2",
"artwork": {"url": "https://example.com/images/2/{w}x{h}.{f}"},
"latestVersionInfo": {"versionDisplay": "2.0.0"}
}
},
"deviceFamilies": ["mac", "iphone", "ipad"]
}
}`,
// ipados app
"3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3,
"supportedDevices": ["iPadAir-iPadAir"] }`,
"3": `{
"id": "3",
"attributes": {
"name": "App 3",
"platformAttributes": {
"ios": {
"bundleId": "c-3",
"artwork": {"url": "https://example.com/images/3/{w}x{h}.{f}"},
"latestVersionInfo": {"versionDisplay": "3.0.0"}
}
},
"deviceFamilies": ["ipad"]
}
}`,
}

adamIDString := r.URL.Query().Get("id")
adamIDString := r.URL.Query().Get("ids")
adamIDs := strings.Split(adamIDString, ",")

var objs []string
for _, a := range adamIDs {
objs = append(objs, db[a])
if obj, ok := db[a]; ok {
objs = append(objs, obj)
}
}

_, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ","))))
_, _ = w.Write(fmt.Appendf(nil, `{"data": [%s]}`, strings.Join(objs, ",")))
}))
t.Setenv("FLEET_DEV_ITUNES_URL", appleITunesSrv.URL)

vppProxyAuthSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"fleetServerSecret": "test-bearer-token"}`))
}))

t.Cleanup(vppProxySrv.Close)
t.Cleanup(vppProxyAuthSrv.Close)
t.Setenv("FLEET_DEV_STOKEN_AUTHENTICATED_APPS_URL", vppProxySrv.URL)
t.Setenv("FLEET_DEV_VPP_PROXY_AUTH_URL", vppProxyAuthSrv.URL)
}

type MockPusher struct{}
Expand Down
30 changes: 30 additions & 0 deletions cmd/fleetctl/integrationtest/gitops/software_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ func TestGitOpsTeamSoftwareInstallers(t *testing.T) {
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
return nil, 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
}

_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", c.file})
if c.wantErr == "" {
Expand Down Expand Up @@ -301,6 +307,12 @@ func TestGitOpsNoTeamVPPPolicies(t *testing.T) {
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
}

t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
globalFile := "../../fleetctl/testdata/gitops/global_config_no_paths.yml"
Expand Down Expand Up @@ -431,6 +443,12 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
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
}

t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
globalFile := "../../fleetctl/testdata/gitops/global_config_no_paths.yml"
Expand Down Expand Up @@ -576,6 +594,12 @@ func TestGitOpsTeamVPPApps(t *testing.T) {
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
return nil, 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
}

_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", c.file})

Expand Down Expand Up @@ -650,6 +674,12 @@ func TestGitOpsTeamVPPAndApp(t *testing.T) {
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
}

buf, err := fleetctl.RunAppNoChecks([]string{
"gitops", "-f", "../../fleetctl/testdata/gitops/global_config_vpp.yml", "-f",
Expand Down
Loading
Loading